diff --git a/application/Espo/Classes/FieldValidators/ArrayType.php b/application/Espo/Classes/FieldValidators/ArrayType.php index f580f8d6c1..faf4dbc8a1 100644 --- a/application/Espo/Classes/FieldValidators/ArrayType.php +++ b/application/Espo/Classes/FieldValidators/ArrayType.php @@ -40,7 +40,7 @@ class ArrayType { private const DEFAULT_MAX_ITEM_LENGTH = 100; - public function __construct(private Metadata $metadata, private Defs $defs) + public function __construct(protected Metadata $metadata, private Defs $defs) {} public function checkRequired(Entity $entity, string $field): bool diff --git a/application/Espo/Classes/FieldValidators/UrlMultipleType.php b/application/Espo/Classes/FieldValidators/UrlMultipleType.php new file mode 100644 index 0000000000..31e3e6222a --- /dev/null +++ b/application/Espo/Classes/FieldValidators/UrlMultipleType.php @@ -0,0 +1,55 @@ +metadata->get(['app', 'regExpPatterns', 'uriOptionalProtocol', 'pattern']); + + return parent::checkPattern($entity, $field, $pattern); + } +} diff --git a/application/Espo/Repositories/ArrayValue.php b/application/Espo/Repositories/ArrayValue.php index ddf7ab235f..420d10f4b8 100644 --- a/application/Espo/Repositories/ArrayValue.php +++ b/application/Espo/Repositories/ArrayValue.php @@ -30,20 +30,22 @@ namespace Espo\Repositories; use Espo\Core\ORM\Entity as CoreEntity; +use Espo\Entities\ArrayValue as ArrayValueEntity; use Espo\ORM\Entity; - use Espo\Core\Repositories\Database; use RuntimeException; use LogicException; /** - * @extends Database<\Espo\Entities\ArrayValue> + * @extends Database */ class ArrayValue extends Database { protected $hooksDisabled = true; + private const ITEM_MAX_LENGTH = 100; + public function storeEntityAttribute(CoreEntity $entity, string $attribute, bool $populateMode = false): void { if ($entity->getAttributeType($attribute) !== Entity::JSON_ARRAY) { @@ -73,7 +75,6 @@ class ArrayValue extends Database } $valueList = array_unique($valueList); - $toSkipValueList = []; $isTransaction = false; @@ -104,6 +105,12 @@ class ArrayValue extends Database } } + $itemMaxLength = $this->entityManager + ->getDefs() + ->getEntity(ArrayValueEntity::ENTITY_TYPE) + ->getField('value') + ->getParam('maxLength') ?? self::ITEM_MAX_LENGTH; + foreach ($valueList as $value) { if (in_array($value, $toSkipValueList)) { continue; @@ -113,6 +120,10 @@ class ArrayValue extends Database continue; } + if (strlen($value) > $itemMaxLength) { + $value = substr($value, 0, $itemMaxLength); + } + $arrayValue = $this->getNew(); $arrayValue->set([ diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 9fdf20b8f2..bc33df4228 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -136,6 +136,7 @@ "phone": "Phone", "text": "Text", "url": "Url", + "urlMultiple": "Url Multiple", "varchar": "Varchar", "file": "File", "image": "Image", diff --git a/application/Espo/Resources/i18n/en_US/FieldManager.json b/application/Espo/Resources/i18n/en_US/FieldManager.json index 73c7f14540..b4301c0bb4 100644 --- a/application/Espo/Resources/i18n/en_US/FieldManager.json +++ b/application/Espo/Resources/i18n/en_US/FieldManager.json @@ -144,6 +144,7 @@ "array": "A list of values, similar to Multi-Enum field.", "address": "An address with street, city, state, postal code and country.", "url": "For storing links.", + "urlMultiple": "Multiple links.", "wysiwyg": "A text with HTML support.", "file": "For file uploading.", "image": "For image uploading.", diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 8a6158ab92..c76273d029 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -308,6 +308,7 @@ "fieldNotMatchingPattern$latinLettersDigitsWhitespace": "{field} can contain only latin letters, digits and whitespace", "fieldNotMatchingPattern$latinLettersWhitespace": "{field} can contain only latin letters and whitespace", "fieldNotMatchingPattern$digits": "{field} can contain only digits", + "fieldNotMatchingPattern$uriOptionalProtocol": "{field} must be a valid URL", "fieldInvalid": "{field} is invalid", "fieldIsRequired": "{field} is required", "fieldPhoneInvalidCharacters": "Only digits, latin letters and characters `-+_@:#().` are allowed", diff --git a/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json b/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json index c36f263faf..b0fb809073 100644 --- a/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json +++ b/application/Espo/Resources/metadata/clientDefs/DynamicLogic.json @@ -247,6 +247,10 @@ "checklist": { "view": "views/admin/dynamic-logic/conditions/field-types/multi-enum", "typeList": ["isEmpty", "isNotEmpty", "has", "notHas"] + }, + "urlMultiple": { + "view": "views/admin/dynamic-logic/conditions/field-types/multi-enum", + "typeList": ["isEmpty", "isNotEmpty"] } }, "conditionTypes": { @@ -317,4 +321,4 @@ "valueType": "varchar" } } -} \ No newline at end of file +} diff --git a/application/Espo/Resources/metadata/fields/urlMultiple.json b/application/Espo/Resources/metadata/fields/urlMultiple.json new file mode 100644 index 0000000000..64bef5c013 --- /dev/null +++ b/application/Espo/Resources/metadata/fields/urlMultiple.json @@ -0,0 +1,54 @@ +{ + "params": [ + { + "name": "required", + "type": "bool", + "default": false + }, + { + "name": "maxCount", + "type": "int", + "min": 1, + "tooltip": true + }, + { + "name": "strip", + "type": "bool", + "default": false, + "tooltip": "urlStrip" + }, + { + "name": "audited", + "type": "bool" + }, + { + "name": "readOnly", + "type": "bool" + } + ], + "validationList": [ + "required", + "maxCount" + ], + "mandatoryValidationList": [ + "array", + "arrayOfString", + "valid", + "maxItemLength", + "pattern", + "noEmptyString" + ], + "filter": true, + "notCreatable": false, + "notSortable": true, + "fieldDefs": { + "type": "jsonArray", + "storeArrayValues": true + }, + "personalData": true, + "massUpdateActionList": [ + "update", + "add", + "remove" + ] +} diff --git a/client/src/views/fields/array.js b/client/src/views/fields/array.js index c35e95a64e..ba4e315475 100644 --- a/client/src/views/fields/array.js +++ b/client/src/views/fields/array.js @@ -386,7 +386,7 @@ function (Dep, RegExpPattern, /** module:ui/multi-select*/MultiSelect) { this.trigger('change'); }, distance: 5, - cancel: 'input,textarea,button,select,option,a', + cancel: 'input,textarea,button,select,option,a[role="button"]', cursor: 'grabbing', }); } @@ -604,9 +604,10 @@ function (Dep, RegExpPattern, /** module:ui/multi-select*/MultiSelect) { ) .append( $('') + .addClass('text') .text(text) - .append(' ') ) + .append('') .get(0) .outerHTML; }, diff --git a/client/src/views/fields/url-multiple.js b/client/src/views/fields/url-multiple.js new file mode 100644 index 0000000000..7998d56d0c --- /dev/null +++ b/client/src/views/fields/url-multiple.js @@ -0,0 +1,125 @@ +/************************************************************************ + * 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. + ************************************************************************/ + +define('views/fields/url-multiple', ['views/fields/array'], function (Dep) { + + /** + * An Url-Multiple field. + * + * @class + * @name Class + * @extends module:views/fields/array.Class + * @memberOf module:views/fields/url-multiple + */ + return Dep.extend(/** @lends module:views/fields/url-multiple.Class# */{ + + type: 'urlMultiple', + + maxItemLength: 255, + displayAsList: true, + defaultProtocol: 'https:', + + setup: function () { + Dep.prototype.setup.call(this); + + this.noEmptyString = true; + this.params.pattern = '$uriOptionalProtocol'; + }, + + addValueFromUi: function (value) { + value = value.trim(); + + if (this.params.strip) { + value = this.strip(value); + } + + if (value === decodeURI(value)) { + value = encodeURI(value); + } + + Dep.prototype.addValueFromUi.call(this, value); + }, + + /** + * @param {string} value + * @return {string} + */ + strip: function (value) { + if (value.indexOf('//') !== -1) { + value = value.substring(value.indexOf('//') + 2); + } + + value = value.replace(/\/+$/, ''); + + return value; + }, + + prepareUrl: function (url) { + if (url.indexOf('//') === -1) { + url = this.defaultProtocol + '//' + url; + } + + return url; + }, + + getValueForDisplay: function () { + /** @type {JQuery[]} */ + let $list = this.selected.map(value => { + return $('') + .attr('href', this.prepareUrl(value)) + .attr('target', '_blank') + .text(decodeURI(value)); + }); + + return $list + .map($item => + $('
') + .addClass('multi-enum-item-container') + .append($item) + .get(0).outerHTML + ) + .join(''); + }, + + getItemHtml: function (value) { + let html = Dep.prototype.getItemHtml.call(this, value); + + let $item = $(html); + + $item.find('span.text').html( + $('') + .attr('href', this.prepareUrl(value)) + .css('user-drag', 'none') + .attr('target', '_blank') + .text(decodeURI(value)) + ); + + return $item.get(0).outerHTML; + }, + }); +}); diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less index 3b54735f59..830a29b768 100644 --- a/frontend/less/espo/custom.less +++ b/frontend/less/espo/custom.less @@ -1132,8 +1132,6 @@ input.global-search-input { } } - - .link-container { > .list-group-item { .link-item-column { @@ -1151,18 +1149,18 @@ input.global-search-input { } } -.filter .field .link-container .list-group-item a { +.filter .field .link-container .list-group-item a[role="button"] { margin-top: 1px; margin-left: 2px; } -.field .link-container .list-group-item a { +.field .link-container .list-group-item a[role="button"] { margin-top: 1px; margin-left: 2px; } -.field .link-container > .list-group-item.link-with-role > a, -.field .link-container > .list-group-item.link-with-role > div > a { +.field .link-container > .list-group-item.link-with-role > a[role="button"], +.field .link-container > .list-group-item.link-with-role > div > a[role="button"] { margin-top: 6px; }