From f3f797a7cf9a2a6607e6aa74e2a31b2acfe2893a Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 9 Nov 2023 17:25:23 +0200 Subject: [PATCH] phone number only numeric search param --- .../DuplicateWhereBuilders/General.php | 20 ++- .../Espo/Core/Select/Text/ConfigProvider.php | 5 + .../Espo/Core/Select/Text/DefaultFilter.php | 19 +++ .../Espo/Resources/defaults/config.php | 1 + .../Espo/Resources/i18n/en_US/Settings.json | 1 + .../Resources/layouts/Settings/settings.json | 3 +- .../metadata/entityDefs/Settings.json | 3 + client/src/views/fields/phone.js | 135 +++++++++++++----- 8 files changed, 144 insertions(+), 43 deletions(-) diff --git a/application/Espo/Classes/DuplicateWhereBuilders/General.php b/application/Espo/Classes/DuplicateWhereBuilders/General.php index 3af1f5f7f0..c0305b5e91 100644 --- a/application/Espo/Classes/DuplicateWhereBuilders/General.php +++ b/application/Espo/Classes/DuplicateWhereBuilders/General.php @@ -33,6 +33,7 @@ use Espo\Core\Duplicate\WhereBuilder; use Espo\Core\Field\EmailAddressGroup; use Espo\Core\Field\PhoneNumberGroup; use Espo\Core\ORM\Entity as CoreEntity; +use Espo\Core\Utils\Config; use Espo\Core\Utils\Metadata; use Espo\ORM\Defs; use Espo\ORM\Entity; @@ -49,7 +50,8 @@ class General implements WhereBuilder { public function __construct( private Metadata $metadata, - private Defs $ormDefs + private Defs $ormDefs, + private Config $config ) {} /** @@ -177,6 +179,12 @@ class General implements WhereBuilder $toCheck = false; + $isNumeric = $this->config->get('phoneNumberNumericSearch'); + + $column = $isNumeric ? + $field . 'Numeric' : + $field; + if ( ($entity->get($field) || $entity->get($field . 'Data')) && ( @@ -185,11 +193,15 @@ class General implements WhereBuilder $entity->isAttributeChanged($field . 'Data') ) ) { - foreach ($this->getPhoneNumberList($entity) as $phoneNumber) { + foreach ($this->getPhoneNumberList($entity) as $number) { + if ($isNumeric) { + $number = preg_replace('/[^0-9]/', '', $number); + } + $orBuilder->add( Cond::equal( - Cond::column($field), - $phoneNumber + Cond::column($column), + $number ) ); diff --git a/application/Espo/Core/Select/Text/ConfigProvider.php b/application/Espo/Core/Select/Text/ConfigProvider.php index b2f0173dcb..08e7e6f5f4 100644 --- a/application/Espo/Core/Select/Text/ConfigProvider.php +++ b/application/Espo/Core/Select/Text/ConfigProvider.php @@ -48,4 +48,9 @@ class ConfigProvider { return $this->config->get('textFilterUseContainsForVarchar') ?? false; } + + public function usePhoneNumberNumericSearch(): bool + { + return $this->config->get('phoneNumberNumericSearch') ?? false; + } } diff --git a/application/Espo/Core/Select/Text/DefaultFilter.php b/application/Espo/Core/Select/Text/DefaultFilter.php index 18bab91708..24f81847d6 100644 --- a/application/Espo/Core/Select/Text/DefaultFilter.php +++ b/application/Espo/Core/Select/Text/DefaultFilter.php @@ -112,6 +112,25 @@ class DefaultFilter implements Filter return; } + if ( + !str_contains($attribute, '.') && + $this->metadataProvider->getFieldType($this->entityType, $attribute) === 'phone' + ) { + if (str_contains($filter, ' ')) { + return; + } + + if ($this->config->usePhoneNumberNumericSearch()) { + $attribute = $attribute . 'Numeric'; + + $filter = preg_replace('/[^0-9]/', '', $filter); + } + + if (!$filter) { + return; + } + } + $expression = $filter; if (!$skipWildcards) { diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index 19aa2480f4..19e11c93d5 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -279,5 +279,6 @@ return [ 'oidcScopes' => ['profile', 'email', 'phone'], 'listViewSettingsDisabled' => false, 'cleanupDeletedRecords' => true, + 'phoneNumberNumericSearch' => false, 'isInstalled' => false, ]; diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index 07e1c117b0..da7f21e63b 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -109,6 +109,7 @@ "adminNotificationsNewVersion": "Show notification when new EspoCRM version is available", "adminNotificationsNewExtensionVersion": "Show notification when new versions of extensions are available", "textFilterUseContainsForVarchar": "Use 'contains' operator when filtering varchar fields", + "phoneNumberNumericSearch": "Numeric phone number search", "authTokenPreventConcurrent": "Only one auth token per user", "scopeColorsDisabled": "Disable scope colors", "tabColorsDisabled": "Disable tab colors", diff --git a/application/Espo/Resources/layouts/Settings/settings.json b/application/Espo/Resources/layouts/Settings/settings.json index 92eadd23ad..e8188f5972 100644 --- a/application/Espo/Resources/layouts/Settings/settings.json +++ b/application/Espo/Resources/layouts/Settings/settings.json @@ -10,7 +10,8 @@ { "label": "Search", "rows": [ - [{"name": "textFilterUseContainsForVarchar"}, {"name": "globalSearchEntityList"}] + [{"name": "textFilterUseContainsForVarchar"}, {"name": "globalSearchEntityList"}], + [{"name": "phoneNumberNumericSearch"}, false] ] }, { diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index 6463840845..4371445207 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -688,6 +688,9 @@ "type": "bool", "tooltip": true }, + "phoneNumberNumericSearch": { + "type": "bool" + }, "scopeColorsDisabled": { "type": "bool" }, diff --git a/client/src/views/fields/phone.js b/client/src/views/fields/phone.js index f03c913e1d..c36ed98e90 100644 --- a/client/src/views/fields/phone.js +++ b/client/src/views/fields/phone.js @@ -42,9 +42,9 @@ class PhoneFieldView extends VarcharFieldView { events = { /** @this PhoneFieldView */ 'click [data-action="switchPhoneProperty"]': function (e) { - let $target = $(e.currentTarget); - let $block = $(e.currentTarget).closest('div.phone-number-block'); - let property = $target.data('property-type'); + const $target = $(e.currentTarget); + const $block = $(e.currentTarget).closest('div.phone-number-block'); + const property = $target.data('property-type'); if (property === 'primary') { if (!$target.hasClass('active')) { @@ -68,13 +68,13 @@ class PhoneFieldView extends VarcharFieldView { }, /** @this PhoneFieldView */ 'click [data-action="removePhoneNumber"]': function (e) { - let $block = $(e.currentTarget).closest('div.phone-number-block'); + const $block = $(e.currentTarget).closest('div.phone-number-block'); this.removePhoneNumber($block); this.trigger('change'); - let $last = this.$el.find('.phone-number').last(); + const $last = this.$el.find('.phone-number').last(); if ($last.length) { $last[0].focus({preventScroll: true}); @@ -82,8 +82,8 @@ class PhoneFieldView extends VarcharFieldView { }, /** @this PhoneFieldView */ 'change input.phone-number': function (e) { - let $input = $(e.currentTarget); - let $block = $input.closest('div.phone-number-block'); + const $input = $(e.currentTarget); + const $block = $input.closest('div.phone-number-block'); if (this._itemJustRemoved) { return; @@ -112,9 +112,9 @@ class PhoneFieldView extends VarcharFieldView { }, /** @this PhoneFieldView */ 'keydown input.phone-number': function (e) { - let key = Espo.Utils.getKeyFromKeyEvent(e); + const key = Espo.Utils.getKeyFromKeyEvent(e); - let $target = $(e.currentTarget); + const $target = $(e.currentTarget); if (key === 'Enter') { if (!this.$el.find('[data-action="addPhoneNumber"]').hasClass('disabled')) { @@ -127,7 +127,7 @@ class PhoneFieldView extends VarcharFieldView { } if (key === 'Backspace' && $target.val() === '') { - let $block = $target.closest('div.phone-number-block'); + const $block = $target.closest('div.phone-number-block'); this._itemJustRemoved = true; setTimeout(() => this._itemJustRemoved = false, 100); @@ -147,7 +147,7 @@ class PhoneFieldView extends VarcharFieldView { } if (!this.model.get(this.name)) { - let msg = this.translate('fieldIsRequired', 'messages') + const msg = this.translate('fieldIsRequired', 'messages') .replace('{field}', this.getLabelText()); this.showValidationMessage(msg, 'div.phone-number-block:nth-child(1) input.phone-number'); @@ -158,37 +158,37 @@ class PhoneFieldView extends VarcharFieldView { // noinspection JSUnusedGlobalSymbols validatePhoneData() { - let data = this.model.get(this.dataFieldName); + const data = this.model.get(this.dataFieldName); if (!data || !data.length) { return; } /** @var {string} */ - let pattern = '^' + this.getMetadata().get(['app', 'regExpPatterns', 'phoneNumberLoose', 'pattern']) + '$'; - let regExp = new RegExp(pattern); + const pattern = '^' + this.getMetadata().get(['app', 'regExpPatterns', 'phoneNumberLoose', 'pattern']) + '$'; + const regExp = new RegExp(pattern); - let numberList = []; + const numberList = []; let notValid = false; data.forEach((row, i) => { - let number = row.phoneNumber; + const msg = this.translate('fieldValueDuplicate', 'messages') + .replace('{field}', this.getLabelText()); + const number = row.phoneNumber; if (!regExp.test(number)) { notValid = true; - let msg = this.translate('fieldPhoneInvalidCharacters', 'messages') + 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'); } - let numberClean = String(number).replace(/[\s+]/g, ''); + const numberClean = String(number).replace(/[\s+]/g, ''); if (~numberList.indexOf(numberClean)) { - let msg = this.translate('fieldValueDuplicate', 'messages') - .replace('{field}', this.getLabelText()); this.showValidationMessage(msg, 'div.phone-number-block:nth-child(' + (i + 1) .toString() + ') input.phone-number'); @@ -207,6 +207,7 @@ class PhoneFieldView extends VarcharFieldView { } data() { + const number = this.model.get(this.name); let phoneNumberData; if (this.mode === this.MODE_EDIT) { @@ -239,7 +240,7 @@ class PhoneFieldView extends VarcharFieldView { phoneNumberData = Espo.Utils.cloneDeep(phoneNumberData); phoneNumberData.forEach((item) => { - let number = item.phoneNumber || ''; + const number = item.phoneNumber || ''; item.erased = number.indexOf(this.erasedPlaceholder) === 0; @@ -252,9 +253,8 @@ class PhoneFieldView extends VarcharFieldView { } if ((!phoneNumberData || phoneNumberData.length === 0) && this.model.get(this.name)) { - let number = this.model.get(this.name); - let o = { + const o = { phoneNumber: number, primary: true, valueForLink: number.replace(/ /g, ''), @@ -267,7 +267,7 @@ class PhoneFieldView extends VarcharFieldView { phoneNumberData = [o]; } - let data = { + const data = { ...super.data(), phoneNumberData: phoneNumberData, doNotCall: this.model.get('doNotCall'), @@ -295,7 +295,7 @@ class PhoneFieldView extends VarcharFieldView { } focusOnLast(cursorAtEnd) { - let $item = this.$el.find('input.form-control').last(); + const $item = this.$el.find('input.form-control').last(); $item.focus(); @@ -315,9 +315,9 @@ class PhoneFieldView extends VarcharFieldView { } addPhoneNumber() { - let data = Espo.Utils.cloneDeep(this.fetchPhoneNumberData()); + const data = Espo.Utils.cloneDeep(this.fetchPhoneNumberData()); - let o = { + const o = { phoneNumber: '', primary: !data.length, type: false, @@ -346,6 +346,12 @@ class PhoneFieldView extends VarcharFieldView { } } + afterRenderSearch() { + super.afterRenderSearch(); + + // @todo If numeric search enabled, allow only digits to enter. + } + removePhoneNumberBlock($block) { let changePrimary = false; @@ -368,7 +374,7 @@ class PhoneFieldView extends VarcharFieldView { } manageAddButton() { - let $input = this.$el.find('input.phone-number'); + const $input = this.$el.find('input.phone-number'); let c = 0; $input.each((i, input) => { @@ -391,9 +397,9 @@ class PhoneFieldView extends VarcharFieldView { } manageButtonsVisibility() { - let $primary = this.$el.find('button[data-property-type="primary"]'); - let $remove = this.$el.find('button[data-action="removePhoneNumber"]'); - let $container = this.$el.find('.phone-number-block-container'); + const $primary = this.$el.find('button[data-property-type="primary"]'); + const $remove = this.$el.find('button[data-action="removePhoneNumber"]'); + const $container = this.$el.find('.phone-number-block-container'); if ($primary.length > 1) { $primary.removeClass('hidden'); @@ -440,14 +446,14 @@ class PhoneFieldView extends VarcharFieldView { } fetchPhoneNumberData() { - let data = []; + const data = []; - let $list = this.$el.find('div.phone-number-block'); + const $list = this.$el.find('div.phone-number-block'); if ($list.length) { $list.each((i, d) => { - let row = {}; - let $d = $(d); + const row = {}; + const $d = $(d); row.phoneNumber = $d.find('input.phone-number').val().trim(); @@ -468,9 +474,9 @@ class PhoneFieldView extends VarcharFieldView { } fetch() { - let data = {}; + const data = {}; - let addressData = this.fetchPhoneNumberData() || []; + const addressData = this.fetchPhoneNumberData() || []; data[this.dataFieldName] = addressData; data[this.name] = null; @@ -494,7 +500,7 @@ class PhoneFieldView extends VarcharFieldView { }); if (addressData.length && primaryIndex > 0) { - let t = addressData[0]; + const t = addressData[0]; addressData[0] = addressData[primaryIndex]; addressData[primaryIndex] = t; @@ -509,6 +515,59 @@ class PhoneFieldView extends VarcharFieldView { return data; } + + /** @inheritDoc */ + fetchSearch() { + const type = this.fetchSearchType() || 'startsWith'; + + const isNumeric = this.getConfig().get('phoneNumberNumericSearch'); + + const name = isNumeric ? + this.name + 'Numeric' : + this.name; + + if (~['isEmpty', 'isNotEmpty'].indexOf(type)) { + if (type === 'isEmpty') { + return { + type: 'isNull', + attribute: name, + data: { + type: type, + }, + }; + } + + return { + type: 'isNotNull', + attribute: name, + data: { + type: type, + }, + }; + } + + /** @type {string} */ + let value = this.$element.val() + .toString() + .trim(); + + if (isNumeric && value) { + value = value.replace(/[^0-9]/g, ''); + } + + if (!value) { + return null; + } + + return { + type: type, + value: value, + attribute: name, + data: { + type: type, + }, + }; + } } export default PhoneFieldView;