This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
espocrm-base/client/src/views/fields/array.js
T
Yuri Kuznetsov 6da9ac6af1 enum label type
2024-04-11 13:17:07 +03:00

839 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/************************************************************************
* 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.
************************************************************************/
/** @module views/fields/array */
import BaseFieldView from 'views/fields/base';
import RegExpPattern from 'helpers/reg-exp-pattern';
import MultiSelect from 'ui/multi-select';
/**
* An array field.
*/
class ArrayFieldView extends BaseFieldView {
type = 'array'
listTemplate = 'fields/array/list'
listLinkTemplate = 'fields/array/list-link'
detailTemplate = 'fields/array/detail'
editTemplate = 'fields/array/edit'
searchTemplate = 'fields/array/search'
searchTypeList = ['anyOf', 'noneOf', 'allOf', 'isEmpty', 'isNotEmpty']
maxItemLength = null
validations = ['required', 'maxCount']
MAX_ITEM_LENGTH = 100
/**
* An add-item model view.
*
* @protected
* @type {string}
*/
addItemModalView = 'views/modals/array-field-add'
/**
* @protected
* @type {string}
*/
itemDelimiter = ':,:'
/**
* @protected
* @type {boolean}
*/
matchAnyWord = true
/**
* @protected
* @type {Object|null}
*/
translatedOptions = null
/** @inheritDoc */
data() {
const itemHtmlList = [];
(this.selected || []).forEach(value => {
itemHtmlList.push(this.getItemHtml(value));
});
// noinspection JSValidateTypes
return {
...super.data(),
selected: this.selected,
translatedOptions: this.translatedOptions,
hasOptions: !!this.params.options,
itemHtmlList: itemHtmlList,
isEmpty: (this.selected || []).length === 0,
valueIsSet: this.model.has(this.name),
maxItemLength: this.maxItemLength || this.MAX_ITEM_LENGTH,
allowCustomOptions: this.allowCustomOptions,
};
}
/** @inheritDoc */
events = {
/** @this ArrayFieldView */
'click [data-action="removeValue"]': function (e) {
const value = $(e.currentTarget).attr('data-value').toString();
this.removeValue(value);
this.focusOnElement();
},
/** @this ArrayFieldView */
'click [data-action="showAddModal"]': function () {
this.actionAddItem();
},
}
setup() {
super.setup();
this.noEmptyString = this.params.noEmptyString;
this.listenTo(this.model, 'change:' + this.name, () => {
this.selected = Espo.Utils.clone(this.model.get(this.name)) || [];
});
this.selected = Espo.Utils.clone(this.model.get(this.name) || []);
if (Object.prototype.toString.call(this.selected) !== '[object Array]') {
this.selected = [];
}
let optionsPath = this.params.optionsPath;
/** @type {?string} */
const optionsReference = this.params.optionsReference;
if (!optionsPath && optionsReference) {
const [refEntityType, refField] = optionsReference.split('.');
optionsPath = `entityDefs.${refEntityType}.fields.${refField}.options`;
}
if (optionsPath) {
this.params.options = Espo.Utils.clone(this.getMetadata().get(optionsPath)) || [];
}
this.styleMap = this.params.style || {};
this.setupOptions();
if ('translatedOptions' in this.options) {
this.translatedOptions = this.options.translatedOptions;
}
if ('translatedOptions' in this.params) {
this.translatedOptions = this.params.translatedOptions;
}
if (!this.translatedOptions) {
this.setupTranslation();
}
this.displayAsLabel = this.params.displayAsLabel || this.displayAsLabel;
this.displayAsList = this.params.displayAsList || this.displayAsList;
if (this.params.isSorted && this.translatedOptions) {
this.params.options = Espo.Utils.clone(this.params.options);
this.params.options = this.params.options.sort((v1, v2) => {
return (this.translatedOptions[v1] || v1).localeCompare(this.translatedOptions[v2] || v2);
});
}
if (this.options.customOptionList) {
this.setOptionList(this.options.customOptionList, true);
}
if (this.params.allowCustomOptions || !this.params.options) {
this.allowCustomOptions = true;
}
}
focusOnElement() {
const $button = this.$el.find('button[data-action="showAddModal"]');
if ($button[0]) {
// noinspection JSUnresolvedReference
$button[0].focus({preventScroll: true});
return;
}
const $input = this.$el.find('input.main-element');
if ($input[0]) {
// noinspection JSUnresolvedReference
$input[0].focus({preventScroll: true});
}
}
setupSearch() {
this.events['change select.search-type'] = e => {
this.handleSearchType($(e.currentTarget).val());
};
}
handleSearchType(type) {
const $inputContainer = this.$el.find('div.input-container');
if (~['anyOf', 'noneOf', 'allOf'].indexOf(type)) {
$inputContainer.removeClass('hidden');
} else {
$inputContainer.addClass('hidden');
}
}
setupTranslation() {
let obj = {};
let translation = this.params.translation;
/** @type {?string} */
const optionsReference = this.params.optionsReference;
if (!translation && optionsReference) {
const [refEntityType, refField] = optionsReference.split('.');
translation = `${refEntityType}.options.${refField}`;
}
this.translatedOptions = null;
if (!this.params.options) {
return;
}
obj = translation ?
this.getLanguage().translatePath(translation) :
this.translate(this.name, 'options', this.model.name);
const map = {};
this.params.options.forEach(o => {
if (typeof obj === 'object' && o in obj) {
map[o] = obj[o];
return;
}
map[o] = o;
});
this.translatedOptions = map;
}
setupOptions() {}
setOptionList(optionList, silent) {
const previousOptions = this.params.options;
if (!this.originalOptionList) {
this.originalOptionList = this.params.options;
}
this.params.options = Espo.Utils.clone(optionList);
const isChanged = !_(previousOptions).isEqual(optionList);
if (this.isEditMode() && !silent && isChanged) {
const selectedOptionList = [];
this.selected.forEach(option => {
if (~optionList.indexOf(option)) {
selectedOptionList.push(option);
}
});
this.selected = selectedOptionList;
if (this.isRendered()) {
this.reRender();
this.trigger('change');
}
else {
this.once('after:render', () => {
this.trigger('change');
});
}
}
}
setTranslatedOptions(translatedOptions) {
this.translatedOptions = translatedOptions;
}
resetOptionList() {
if (!this.originalOptionList) {
return;
}
const previousOptions = this.params.options;
this.params.options = Espo.Utils.clone(this.originalOptionList);
const isChanged = !_(previousOptions).isEqual(this.originalOptionList);
if (!this.isEditMode() || !isChanged) {
return;
}
if (this.isRendered()) {
this.reRender();
}
}
controlAddItemButton() {
const $select = this.$select;
if (!$select) {
return;
}
if (!$select.get(0)) {
return;
}
const value = $select.val().toString().trim();
if (!value && this.params.noEmptyString) {
this.$addButton.addClass('disabled').attr('disabled', 'disabled');
}
else {
this.$addButton.removeClass('disabled').removeAttr('disabled');
}
}
afterRender() {
if (this.isEditMode()) {
this.$list = this.$el.find('.list-group');
const $select = this.$select = this.$el.find('.select');
if (this.allowCustomOptions) {
this.$addButton = this.$el.find('button[data-action="addItem"]');
this.$addButton.on('click', () => {
const value = $select.val().toString();
this.addValueFromUi(value);
this.focusOnElement();
});
$select.on('input', () => this.controlAddItemButton());
$select.on('keydown', e => {
const key = Espo.Utils.getKeyFromKeyEvent(e);
if (key === 'Enter') {
const value = $select.val().toString();
this.addValueFromUi(value);
}
});
this.controlAddItemButton();
}
this.$list.sortable({
stop: () => {
this.fetchFromDom();
this.trigger('change');
},
distance: 5,
cancel: 'input,textarea,button,select,option,a[role="button"]',
cursor: 'grabbing',
});
}
if (this.isSearchMode()) {
this.renderSearch();
}
}
/**
* @param {string} value
*/
addValueFromUi(value) {
value = value.trim();
if (this.noEmptyString && value === '') {
return;
}
if (this.params.pattern) {
const helper = new RegExpPattern(this.getMetadata(), this.getLanguage());
const result = helper.validate(this.params.pattern, value, this.name, this.entityType);
if (result) {
setTimeout(() => this.showValidationMessage(result.message, 'input.select'), 10);
return;
}
}
this.addValue(value);
this.$select.val('');
this.controlAddItemButton();
}
renderSearch() {
this.$element = this.$el.find('.main-element');
const valueList = this.getSearchParamsData().valueList || this.searchParams.valueFront || [];
this.$element.val(valueList.join(this.itemDelimiter));
const items = [];
(this.params.options || []).forEach(value => {
let label = this.getLanguage().translateOption(value, this.name, this.scope);
if (this.translatedOptions) {
if (value in this.translatedOptions) {
label = this.translatedOptions[value];
}
}
if (label === '') {
return;
}
items.push({
value: value,
text: label,
});
});
valueList
.filter(item => !(this.params.options || []).includes(item))
.forEach(item => {
items.push({
value: item,
text: item,
});
});
/** @type {module:ui/multi-select~Options} */
const multiSelectOptions = {
items: items,
delimiter: this.itemDelimiter,
matchAnyWord: this.matchAnyWord,
allowCustomOptions: this.allowCustomOptions,
create: input => {
return {
value: input,
text: input,
};
},
};
MultiSelect.init(this.$element, multiSelectOptions);
this.$el.find('.selectize-dropdown-content').addClass('small');
const type = this.$el.find('select.search-type').val();
this.handleSearchType(type);
this.$el.find('select.search-type').on('change', () => {
this.trigger('change');
});
this.$element.on('change', () => {
this.trigger('change');
});
}
fetchFromDom() {
const selected = [];
this.$el.find('.list-group .list-group-item').each((i, el) => {
const value = $(el).attr('data-value').toString();
selected.push(value);
});
this.selected = selected;
}
getValueForDisplay() {
// Do not use the `html` method to avoid XSS.
/** @var {string[]} */
const list = this.selected.map(item => {
let label = null;
if (this.translatedOptions !== null) {
if (item in this.translatedOptions) {
label = this.translatedOptions[item];
}
}
if (label === null) {
label = item;
}
if (label === '') {
label = this.translate('None');
}
const style = this.styleMap[item] || 'default';
if (this.displayAsLabel) {
let className = 'label label-md label-' + style;
if (this.params.labelType === 'state') {
className += ' label-state'
}
return $('<span>')
.addClass(className)
.text(label)
.get(0).outerHTML;
}
if (style && style !== 'default') {
return $('<span>')
.addClass('text-' + style)
.text(label)
.get(0).outerHTML;
}
return $('<span>')
.text(label)
.get(0).outerHTML;
});
if (this.displayAsList) {
if (!list.length) {
return '';
}
let itemClassName = 'multi-enum-item-container';
if (this.displayAsLabel) {
itemClassName += ' multi-enum-item-label-container';
}
return list
.map(item =>
$('<div>')
.addClass(itemClassName)
.html(item)
.get(0).outerHTML
)
.join('');
}
if (this.displayAsLabel) {
return list.join(' ');
}
return list.join(', ');
}
getItemHtml(value) {
// Do not use the `html` method to avoid XSS.
if (this.translatedOptions !== null) {
for (const item in this.translatedOptions) {
if (this.translatedOptions[item] === value) {
value = item;
break;
}
}
}
value = value.toString();
const text = this.translatedOptions && value in this.translatedOptions ?
this.translatedOptions[value].toString() :
value;
return $('<div>')
.addClass('list-group-item')
.attr('data-value', value)
.css('cursor', 'default')
.append(
$('<a>')
.attr('role', 'button')
.attr('tabindex', '0')
.addClass('pull-right')
.attr('data-value', value)
.attr('data-action', 'removeValue')
.append(
$('<span>').addClass('fas fa-times')
)
)
.append(
$('<span>')
.addClass('text')
.text(text)
)
.append('')
.get(0)
.outerHTML;
}
addValue(value) {
if (this.selected.indexOf(value) === -1) {
const html = this.getItemHtml(value);
this.$list.append(html);
this.selected.push(value);
this.trigger('change');
}
}
removeValue(value) {
const valueInternal = value.replace(/"/g, '\\"');
this.$list.children('[data-value="' + valueInternal + '"]').remove();
const index = this.selected.indexOf(value);
this.selected.splice(index, 1);
this.trigger('change');
}
fetch() {
const data = {};
let list = Espo.Utils.clone(this.selected || []);
if (this.params.isSorted && this.translatedOptions) {
list = list.sort((v1, v2) => {
return (this.translatedOptions[v1] || v1)
.localeCompare(this.translatedOptions[v2] || v2);
});
}
data[this.name] = list;
return data;
}
fetchSearch() {
const type = this.$el.find('select.search-type').val() || 'anyOf';
let valueList;
if (~['anyOf', 'noneOf', 'allOf'].indexOf(type)) {
valueList = this.$element.val().split(this.itemDelimiter);
if (valueList.length === 1 && valueList[0] === '') {
valueList = [];
}
if (valueList.length === 0) {
if (type === 'anyOf') {
return {
type: 'any',
data: {
type: type,
valueList: valueList,
},
};
}
if (type === 'noneOf') {
return {
type: 'any',
data: {
type: type,
valueList: valueList,
},
};
}
if (type === 'allOf') {
return {
type: 'any',
data: {
type: type,
valueList: valueList,
},
};
}
}
}
if (type === 'anyOf') {
const data = {
type: 'arrayAnyOf',
value: valueList,
data: {
type: 'anyOf',
valueList: valueList,
},
};
if (!valueList.length) {
data.value = null;
}
return data;
}
if (type === 'noneOf') {
return {
type: 'arrayNoneOf',
value: valueList,
data: {
type: 'noneOf',
valueList: valueList,
},
};
}
if (type === 'allOf') {
const data = {
type: 'arrayAllOf',
value: valueList,
data: {
type: 'allOf',
valueList: valueList,
},
};
if (!valueList.length) {
data.value = null;
}
return data;
}
if (type === 'isEmpty') {
return {
type: 'arrayIsEmpty',
data: {
type: 'isEmpty',
},
};
}
if (type === 'isNotEmpty') {
return {
type: 'arrayIsNotEmpty',
data: {
type: 'isNotEmpty',
},
};
}
return null;
}
validateRequired() {
if (this.isRequired()) {
const value = this.model.get(this.name);
if (!value || value.length === 0) {
const msg = this.translate('fieldIsRequired', 'messages')
.replace('{field}', this.getLabelText());
this.showValidationMessage(msg, '.array-control-container');
return true;
}
}
return false;
}
// noinspection JSUnusedGlobalSymbols
validateMaxCount() {
if (this.params.maxCount) {
const itemList = this.model.get(this.name) || [];
if (itemList.length > this.params.maxCount) {
const msg =
this.translate('fieldExceedsMaxCount', 'messages')
.replace('{field}', this.getLabelText())
.replace('{maxCount}', this.params.maxCount.toString());
this.showValidationMessage(msg, '.array-control-container');
return true;
}
}
return false;
}
getSearchType() {
return this.getSearchParamsData().type || 'anyOf';
}
/**
* @return {{
* translatedOptions: Object.<string, *>|null,
* options: string[],
* } | Object.<string, *>}
*/
getAddItemModalOptions() {
const options = [];
this.params.options.forEach(item => {
if (!~this.selected.indexOf(item)) {
options.push(item);
}
});
return {
options: options,
translatedOptions: this.translatedOptions,
};
}
actionAddItem() {
this.createView('addModal', this.addItemModalView, this.getAddItemModalOptions(), view => {
view.render();
view.once('add', item => {
this.addValue(item);
view.close();
});
view.once('add-mass', items => {
items.forEach(item => this.addValue(item));
view.close();
});
});
}
}
export default ArrayFieldView;