mirror of
https://github.com/espocrm/espocrm.git
synced 2026-06-28 06:56:05 +00:00
phone number extensions
This commit is contained in:
@@ -31,6 +31,7 @@ namespace Espo\Classes\FieldValidators;
|
||||
|
||||
use Brick\PhoneNumber\PhoneNumber;
|
||||
use Brick\PhoneNumber\PhoneNumberParseException;
|
||||
use Espo\Core\PhoneNumber\Util;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\ORM\Defs;
|
||||
@@ -203,6 +204,22 @@ class PhoneType
|
||||
return true;
|
||||
}
|
||||
|
||||
$ext = null;
|
||||
|
||||
if ($this->config->get('phoneNumberExtensions')) {
|
||||
[$number, $ext] = Util::splitExtension($number);
|
||||
}
|
||||
|
||||
if ($ext) {
|
||||
if (!preg_match('/[0-9]+/', $ext)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strlen($ext) > 6) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$numberObj = PhoneNumber::parse($number);
|
||||
}
|
||||
|
||||
@@ -62,13 +62,25 @@ class Sanitizer
|
||||
|
||||
private function parsePhoneNumber(string $value, ?string $countryCode): string
|
||||
{
|
||||
$ext = null;
|
||||
|
||||
if ($this->config->get('phoneNumberExtensions')) {
|
||||
[$value, $ext] = Util::splitExtension($value);
|
||||
}
|
||||
|
||||
try {
|
||||
$number = PhoneNumber::parse($value, $countryCode);
|
||||
|
||||
return (string) $number;
|
||||
}
|
||||
catch (PhoneNumberParseException) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$output = (string) $number;
|
||||
|
||||
if ($ext) {
|
||||
$output .= ' ext. ' . $ext;
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
64
application/Espo/Core/PhoneNumber/Util.php
Normal file
64
application/Espo/Core/PhoneNumber/Util.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU Affero General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Core\PhoneNumber;
|
||||
|
||||
class Util
|
||||
{
|
||||
/**
|
||||
* @internal Do not use in custom code.
|
||||
* @return array{string, ?string}
|
||||
*/
|
||||
public static function splitExtension(string $value): array
|
||||
{
|
||||
$ext = null;
|
||||
|
||||
$delimiters = [
|
||||
'ext.',
|
||||
'x.',
|
||||
'x',
|
||||
'#',
|
||||
];
|
||||
|
||||
foreach ($delimiters as $delimiter) {
|
||||
$index = strrpos($value, $delimiter);
|
||||
|
||||
if ($index === false || $index < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ext = trim(substr($value, $index + strlen($delimiter)));
|
||||
$value = trim(substr($value, 0, $index));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return [$value, $ext];
|
||||
}
|
||||
}
|
||||
@@ -287,6 +287,7 @@ return [
|
||||
'cleanupDeletedRecords' => true,
|
||||
'phoneNumberNumericSearch' => true,
|
||||
'phoneNumberInternational' => true,
|
||||
'phoneNumberExtensions' => false,
|
||||
'phoneNumberPreferredCountryList' => ['us', 'de'],
|
||||
'adminUpgradeDisabled' => false,
|
||||
'wysiwygCodeEditorDisabled' => false,
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
"textFilterUseContainsForVarchar": "Use 'contains' operator when filtering varchar fields",
|
||||
"phoneNumberNumericSearch": "Numeric phone number search",
|
||||
"phoneNumberInternational": "International phone numbers",
|
||||
"phoneNumberExtensions": "Phone number extensions",
|
||||
"phoneNumberPreferredCountryList": "Preferred telephone country codes",
|
||||
"authTokenPreventConcurrent": "Only one auth token per user",
|
||||
"scopeColorsDisabled": "Disable scope colors",
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"label": "Phone Numbers",
|
||||
"rows": [
|
||||
[{"name": "phoneNumberInternational"}, {"name": "phoneNumberPreferredCountryList"}],
|
||||
[{"name": "phoneNumberNumericSearch"}, false]
|
||||
[{"name": "phoneNumberNumericSearch"}, {"name": "phoneNumberExtensions"}]
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -706,6 +706,9 @@
|
||||
"phoneNumberInternational": {
|
||||
"type": "bool"
|
||||
},
|
||||
"phoneNumberExtensions": {
|
||||
"type": "bool"
|
||||
},
|
||||
"phoneNumberPreferredCountryList": {
|
||||
"type": "multiEnum",
|
||||
"view": "views/settings/fields/phone-number-preferred-country-list"
|
||||
|
||||
@@ -26,23 +26,49 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('views/admin/settings', ['views/settings/record/edit'], function (Dep) {
|
||||
import SettingsEditRecordView from 'views/settings/record/edit';
|
||||
|
||||
return Dep.extend({
|
||||
class SettingsAdminRecordView extends SettingsEditRecordView {
|
||||
|
||||
layoutName: 'settings',
|
||||
layoutName = 'settings'
|
||||
|
||||
saveAndContinueEditingAction: false,
|
||||
saveAndContinueEditingAction = false
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
|
||||
if (this.getHelper().getAppParam('isRestrictedMode') && !this.getUser().isSuperAdmin()) {
|
||||
this.hideField('cronDisabled');
|
||||
this.hideField('maintenanceMode');
|
||||
this.setFieldReadOnly('useWebSocket');
|
||||
this.setFieldReadOnly('siteUrl');
|
||||
dynamicLogicDefs = {
|
||||
fields: {
|
||||
phoneNumberPreferredCountryList: {
|
||||
visible: {
|
||||
conditionGroup: [
|
||||
{
|
||||
attribute: 'phoneNumberInternational',
|
||||
type: 'isTrue'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
phoneNumberExtensions: {
|
||||
visible: {
|
||||
conditionGroup: [
|
||||
{
|
||||
attribute: 'phoneNumberInternational',
|
||||
type: 'isTrue'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
if (this.getHelper().getAppParam('isRestrictedMode') && !this.getUser().isSuperAdmin()) {
|
||||
this.hideField('cronDisabled');
|
||||
this.hideField('maintenanceMode');
|
||||
this.setFieldReadOnly('useWebSocket');
|
||||
this.setFieldReadOnly('siteUrl');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SettingsAdminRecordView;
|
||||
|
||||
@@ -297,7 +297,7 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
item.erased = number.indexOf(this.erasedPlaceholder) === 0;
|
||||
|
||||
if (!item.erased) {
|
||||
item.valueForLink = number.replace(/ /g, '');
|
||||
item.valueForLink = this.formatForLink(number);
|
||||
|
||||
if (this.isReadMode()) {
|
||||
item.phoneNumber = this.formatNumber(item.phoneNumber);
|
||||
@@ -312,7 +312,7 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
const o = {
|
||||
phoneNumber: this.formatNumber(number),
|
||||
primary: true,
|
||||
valueForLink: number.replace(/ /g, ''),
|
||||
valueForLink: this.formatForLink(number),
|
||||
};
|
||||
|
||||
if (this.isReadMode()) {
|
||||
@@ -326,8 +326,6 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
phoneNumberData = [o];
|
||||
}
|
||||
|
||||
|
||||
|
||||
const data = {
|
||||
...super.data(),
|
||||
phoneNumberData: phoneNumberData,
|
||||
@@ -343,7 +341,7 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
data.isErased = this.model.get(this.name).indexOf(this.erasedPlaceholder) === 0;
|
||||
|
||||
if (!data.isErased) {
|
||||
data.valueForLink = this.model.get(this.name).replace(/ /g, '');
|
||||
data.valueForLink = this.formatForLink(this.model.get(this.name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +354,18 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} number
|
||||
*/
|
||||
formatForLink(number) {
|
||||
if (this.allowExtensions && this.useInternational) {
|
||||
return number;
|
||||
}
|
||||
|
||||
return number.replace(/ /g, '');
|
||||
}
|
||||
|
||||
focusOnLast(cursorAtEnd) {
|
||||
const $item = this.$el.find('input.form-control').last();
|
||||
|
||||
@@ -449,7 +459,14 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
return;
|
||||
}
|
||||
|
||||
obj.setNumber(obj.getNumber());
|
||||
let number = obj.getNumber();
|
||||
const ext = obj.getExtension();
|
||||
|
||||
if (this.allowExtensions && ext) {
|
||||
number += ' ext. ' + ext;
|
||||
}
|
||||
|
||||
obj.setNumber(number);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -529,6 +546,7 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
|
||||
this.phoneNumberOptedOutByDefault = this.getConfig().get('phoneNumberIsOptedOutByDefault');
|
||||
this.useInternational = this.getConfig().get('phoneNumberInternational') || false;
|
||||
this.allowExtensions = this.getConfig().get('phoneNumberExtensions') || false;
|
||||
this.preferredCountryList = this.getConfig().get('phoneNumberPreferredCountryList') || [];
|
||||
|
||||
if (this.useInternational && !this.isListMode() && !this.isSearchMode()) {
|
||||
@@ -603,6 +621,12 @@ class PhoneFieldView extends VarcharFieldView {
|
||||
|
||||
if (this.intlTelInputMap.has(inputElement)) {
|
||||
row.phoneNumber = this.intlTelInputMap.get(inputElement).getNumber();
|
||||
|
||||
const ext = this.intlTelInputMap.get(inputElement).getExtension() || null;
|
||||
|
||||
if (this.allowExtensions && ext) {
|
||||
row.phoneNumber += ' ext. ' + ext;
|
||||
}
|
||||
}
|
||||
|
||||
if (row.phoneNumber === '') {
|
||||
|
||||
@@ -26,33 +26,30 @@
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
define('views/settings/record/edit', ['views/record/edit'], function (Dep) {
|
||||
import EditRecordView from 'views/record/edit';
|
||||
|
||||
return Dep.extend({
|
||||
class SettingsEditRecordView extends EditRecordView {
|
||||
|
||||
saveAndContinueEditingAction: false,
|
||||
saveAndContinueEditingAction = false
|
||||
|
||||
sideView: null,
|
||||
sideView = null
|
||||
|
||||
layoutName: 'settings',
|
||||
layoutName = 'settings'
|
||||
|
||||
setup: function () {
|
||||
Dep.prototype.setup.call(this);
|
||||
setup() {
|
||||
super.setup();
|
||||
|
||||
this.listenTo(this.model, 'after:save', () => {
|
||||
this.getConfig().set(this.model.getClonedAttributes());
|
||||
});
|
||||
},
|
||||
this.listenTo(this.model, 'after:save', () => {
|
||||
this.getConfig().set(this.model.getClonedAttributes());
|
||||
});
|
||||
}
|
||||
|
||||
afterRender: function () {
|
||||
Dep.prototype.afterRender.call(this);
|
||||
},
|
||||
exit(after) {
|
||||
if (after === 'cancel') {
|
||||
this.getRouter().navigate('#Admin', {trigger: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit: function (after) {
|
||||
if (after === 'cancel') {
|
||||
this.getRouter().navigate('#Admin', {trigger: true});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
export default SettingsEditRecordView;
|
||||
|
||||
|
||||
@@ -31,10 +31,14 @@ namespace tests\integration\Espo\Record;
|
||||
|
||||
use Espo\Core\Application;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Conflict;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Record\CreateParams;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\Modules\Crm\Entities\Lead;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\App\SettingsService as SettingsService;
|
||||
@@ -471,4 +475,47 @@ class FieldValidationTest extends BaseTestCase
|
||||
'opportunityAmount' => 'bad-value',
|
||||
], CreateParams::create());
|
||||
}
|
||||
|
||||
public function testPhoneNumber(): void
|
||||
{
|
||||
$service = $this->getContainer()
|
||||
->getByClass(ServiceContainer::class)
|
||||
->getByClass(Account::class);
|
||||
|
||||
$configWriter = $this->getInjectableFactory()->create(ConfigWriter::class);
|
||||
$configWriter->set('phoneNumberExtensions', true);
|
||||
$configWriter->save();
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$service->create((object)[
|
||||
'name' => 'Test 1',
|
||||
'phoneNumberData' => [
|
||||
(object)[
|
||||
'phoneNumber' => '+38 09 044 433 22 ext. 001',
|
||||
],
|
||||
],
|
||||
], CreateParams::create());
|
||||
|
||||
$thrown = false;
|
||||
|
||||
try {
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$service->create((object)[
|
||||
'name' => 'Test 2',
|
||||
'phoneNumberData' => [
|
||||
(object)[
|
||||
'phoneNumber' => '+38 09 044 433 22 ext. ABC',
|
||||
],
|
||||
(object)[
|
||||
'phoneNumber' => '+38 09 044 433 33 ext. 1234567',
|
||||
],
|
||||
],
|
||||
], CreateParams::create());
|
||||
}
|
||||
catch (BadRequest) {
|
||||
$thrown = true;
|
||||
}
|
||||
|
||||
$this->assertTrue($thrown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace tests\integration\Espo\Record;
|
||||
|
||||
use Espo\Core\Record\CreateParams;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Config\ConfigWriter;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\Modules\Crm\Entities\Meeting;
|
||||
@@ -108,6 +109,44 @@ class SanitizeTest extends BaseTestCase
|
||||
$this->assertEquals('+380904443322', $numbers[0]);
|
||||
$this->assertEquals('+380904443333', $numbers[1]);
|
||||
|
||||
$configWriter = $this->getInjectableFactory()->create(ConfigWriter::class);
|
||||
$configWriter->set('phoneNumberExtensions', true);
|
||||
$configWriter->save();
|
||||
|
||||
/** @var Account $account */
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$account = $service->create((object) [
|
||||
'name' => 'Test 3',
|
||||
'phoneNumberData' => [
|
||||
(object) [
|
||||
'phoneNumber' => '+38 09 044 433 22 ext. 0001',
|
||||
],
|
||||
(object) [
|
||||
'phoneNumber' => '+38 09 044 433 33 x. 1001',
|
||||
],
|
||||
(object) [
|
||||
'phoneNumber' => '+380904443344x.1000',
|
||||
],
|
||||
(object) [
|
||||
'phoneNumber' => '+380904443355#1000',
|
||||
],
|
||||
(object) [
|
||||
'phoneNumber' => '+380904443366 # 1000',
|
||||
],
|
||||
],
|
||||
], CreateParams::create());
|
||||
|
||||
$numbers = $account->getPhoneNumberGroup()->getNumberList();
|
||||
$this->assertCount(5, $numbers);
|
||||
|
||||
sort($numbers);
|
||||
|
||||
$this->assertEquals('+380904443322 ext. 0001', $numbers[0]);
|
||||
$this->assertEquals('+380904443333 ext. 1001', $numbers[1]);
|
||||
$this->assertEquals('+380904443344 ext. 1000', $numbers[2]);
|
||||
$this->assertEquals('+380904443355 ext. 1000', $numbers[3]);
|
||||
$this->assertEquals('+380904443366 ext. 1000', $numbers[4]);
|
||||
|
||||
// datetime
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
Reference in New Issue
Block a user