diff --git a/client/src/views/fields/link-multiple-with-columns.js b/client/src/views/fields/link-multiple-with-columns.js index 1e0e833ceb..4ef2088daa 100644 --- a/client/src/views/fields/link-multiple-with-columns.js +++ b/client/src/views/fields/link-multiple-with-columns.js @@ -26,651 +26,615 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('views/fields/link-multiple-with-columns', -['views/fields/link-multiple', 'helpers/reg-exp-pattern', 'ui/select'], -function (Dep, RegExpPattern, /** module:ui/select*/Select) { +/** @module views/fields/link-multiple-with-columns */ + +import LinkMultipleFieldView from 'views/fields/link-multiple'; +import RegExpPattern from 'helpers/reg-exp-pattern'; +import Select from 'ui/select'; + +/** + * A link-multiple field with relation column(s). + */ +class LinkMultipleWithColumnsFieldView extends LinkMultipleFieldView { + + /** @const */ + COLUMN_TYPE_VARCHAR = 'varchar' + /** @const */ + COLUMN_TYPE_ENUM = 'enum' + /** @const */ + COLUMN_TYPE_BOOL = 'bool' + + /** @inheritDoc */ + setup() { + super.setup(); + + let columnsDefsInitial = this.columnsDefs || {}; + + this.validations.push('columnPattern'); + + /** + * @type {Object.} + */ + this.columnsDefs = {}; + this.columnsName = this.name + 'Columns'; + this.columns = Espo.Utils.cloneDeep(this.model.get(this.columnsName) || {}); + + this.listenTo(this.model, 'change:' + this.columnsName, () => { + this.columns = Espo.Utils.cloneDeep(this.model.get(this.columnsName) || {}); + }); + + let columns = this.getMetadata() + .get(['entityDefs', this.model.entityType, 'fields', this.name, 'columns']) || {}; + + /** @type {string[]} */ + this.columnList = this.columnList || Object.keys(columns); + + this.columnList.forEach(column => { + if (column in columnsDefsInitial) { + this.columnsDefs[column] = Espo.Utils.cloneDeep(columnsDefsInitial[column]); + + return; + } + + if (column in columns) { + let field = columns[column]; + + let o = {}; + + o.field = field; + o.scope = this.foreignScope; + + if ( + !this.getMetadata().get(['entityDefs', this.foreignScope, 'fields', field, 'type']) && + this.getMetadata().get(['entityDefs', this.model.entityType, 'fields', field, 'type']) + ) { + o.scope = this.model.entityType; + } + + let fieldDefs = this.getMetadata().get(['entityDefs', o.scope, 'fields', field]) || {}; + + o.type = fieldDefs.type; + + if (o.type === this.COLUMN_TYPE_ENUM || o.type === this.COLUMN_TYPE_VARCHAR) { + o.options = fieldDefs.options; + } + + if ('default' in fieldDefs) { + o.default = fieldDefs.default; + } + + if ('maxLength' in fieldDefs) { + o.maxLength = fieldDefs.maxLength; + } + + if ('pattern' in fieldDefs) { + o.pattern = fieldDefs.pattern; + } + + this.columnsDefs[column] = o; + } + }); + + if (this.isEditMode() || this.isDetailMode()) { + this.events['click a[data-action="toggleBoolColumn"]'] = (e) => { + let id = $(e.currentTarget).data('id'); + let column = $(e.currentTarget).data('column'); + + this.toggleBoolColumn(id, column); + }; + } + + this.on('render', this.disposeColumnAutocompletes, this); + this.once('remove', this.disposeColumnAutocompletes, this); + } + + toggleBoolColumn(id, column) { + this.columns[id][column] = !this.columns[id][column]; + + this.reRender(); + } + + /** @inheritDoc */ + getAttributeList() { + return [ + ...super.getAttributeList(), + this.name + 'Columns' + ]; + } /** - * A link-multiple field with relation column(s). + * Get an item HTML for detail mode. * - * @class - * @name Class - * @extends module:views/fields/link-multiple.Class - * @memberOf module:views/fields/link-multiple-with-columns + * @param {string} id An ID. + * @param {string} [name] An name. + * @return {string} */ - return Dep.extend(/** @lends module:views/fields/link-multiple-with-columns.Class# */{ + getDetailLinkHtml(id, name) { + // Do not use the `html` method to avoid XSS. - /** - * @const - */ - COLUMN_TYPE_VARCHAR: 'varchar', + name = name || this.nameHash[id] || id; - /** - * @const - */ - COLUMN_TYPE_ENUM: 'enum', + let $el = $('
') + .append( + $('') + .attr('href', '#' + this.foreignScope + '/view/' + id) + .attr('data-id', id) + .text(name) + ); - /** - * @const - */ - COLUMN_TYPE_BOOL: 'bool', + if (this.isDetailMode()) { + let iconHtml = this.getIconHtml(id); - /** - * @inheritDoc - */ - setup: function () { - Dep.prototype.setup.call(this); + if (iconHtml) { + $el.prepend(iconHtml); + } + } - let columnsDefsInitial = this.columnsDefs || {}; + this.columnList.forEach(column => { + let value = (this.columns[id] || {})[column] || ''; + let type = this.columnsDefs[column].type; + let field = this.columnsDefs[column].field; + let scope = this.columnsDefs[column].scope; - this.validations.push('columnPattern'); + if (value === '' || !value) { + return; + } - /** - * @type {Object.} - */ - this.columnsDefs = {}; - this.columnsName = this.name + 'Columns'; - this.columns = Espo.Utils.cloneDeep(this.model.get(this.columnsName) || {}); + if (type !== this.COLUMN_TYPE_ENUM && type !== this.COLUMN_TYPE_VARCHAR) { + return; + } - this.listenTo(this.model, 'change:' + this.columnsName, () => { - this.columns = Espo.Utils.cloneDeep(this.model.get(this.columnsName) || {}); + let text = type === this.COLUMN_TYPE_ENUM ? + this.getLanguage().translateOption(value, field, scope) : + value; + + $el.append( + $('').text(' '), + $('').addClass('text-muted chevron-right'), + $('').text(' '), + $('').text(text).addClass('text-muted small') + ); + }); + + return $el.get(0).outerHTML; + } + + /** @inheritDoc */ + getValueForDisplay() { + if (this.isDetailMode() || this.isListMode()) { + let itemList = []; + + this.ids.forEach(id => { + itemList.push( + this.getDetailLinkHtml(id) + ); }); - let columns = this.getMetadata() - .get(['entityDefs', this.model.entityType, 'fields', this.name, 'columns']) || {}; + return itemList.join(''); + } + } - /** @type {string[]} */ - this.columnList = this.columnList || Object.keys(columns); + /** @inheritDoc */ + deleteLink(id) { + this.trigger('delete-link', id); + this.trigger('delete-link:' + id); + + this.deleteLinkHtml(id); + + let index = this.ids.indexOf(id); + + if (index > -1) { + this.ids.splice(index, 1); + } + + delete this.nameHash[id]; + delete this.columns[id]; + + this.afterDeleteLink(id); + + this.trigger('change'); + } + + /** + * Get a column values. + * + * @param {string} id An ID. + * @param {string} column A column. + * @return {*} + */ + getColumnValue(id, column) { + return (this.columns[id] || {})[column]; + } + + addLink(id, name) { + if (!~this.ids.indexOf(id)) { + this.ids.push(id); + this.nameHash[id] = name; + this.columns[id] = {}; this.columnList.forEach(column => { - if (column in columnsDefsInitial) { - this.columnsDefs[column] = Espo.Utils.cloneDeep(columnsDefsInitial[column]); + this.columns[id][column] = null; - return; - } - - if (column in columns) { - let field = columns[column]; - - let o = {}; - - o.field = field; - o.scope = this.foreignScope; - - if ( - !this.getMetadata().get(['entityDefs', this.foreignScope, 'fields', field, 'type']) && - this.getMetadata().get(['entityDefs', this.model.entityType, 'fields', field, 'type']) - ) { - o.scope = this.model.entityType; - } - - let fieldDefs = this.getMetadata().get(['entityDefs', o.scope, 'fields', field]) || {}; - - o.type = fieldDefs.type; - - if (o.type === this.COLUMN_TYPE_ENUM || o.type === this.COLUMN_TYPE_VARCHAR) { - o.options = fieldDefs.options; - } - - if ('default' in fieldDefs) { - o.default = fieldDefs.default; - } - - if ('maxLength' in fieldDefs) { - o.maxLength = fieldDefs.maxLength; - } - - if ('pattern' in fieldDefs) { - o.pattern = fieldDefs.pattern; - } - - this.columnsDefs[column] = o; + if ('default' in this.columnsDefs[column]) { + this.columns[id][column] = this.columnsDefs[column].default; } }); - if (this.isEditMode() || this.isDetailMode()) { - this.events['click a[data-action="toggleBoolColumn"]'] = (e) => { - let id = $(e.currentTarget).data('id'); - let column = $(e.currentTarget).data('column'); + this.addLinkHtml(id, name); - this.toggleBoolColumn(id, column); + this.afterAddLink(id); + + this.trigger('add-link', id); + this.trigger('add-link:' + id); + } + + this.trigger('change'); + } + + /** + * @param {string} column + * @param {string} id + * @param {*} value + * @return {JQuery} + */ + getJQSelect(column, id, value) { + // Do not use the `html` method to avoid XSS. + + let field = this.columnsDefs[column].field; + let scope = this.columnsDefs[column].scope; + let options = this.columnsDefs[column].options || []; + + let $select = $('') + .addClass('role form-control input-sm') + .attr('data-column', column) + .attr('placeholder', text) + .attr('data-id', id) + .attr('value', value || ''); + + if (maxLength) { + $input.attr('maxlength', maxLength); + } + + return $input; + } + + /** + * @param {string} column + * @param {string} id + * @param {*} value + * @return {JQuery} + */ + getJQLi(column, id, value) { + // Do not use the `html` method to avoid XSS. + + let field = this.columnsDefs[column].field; + let scope = this.columnsDefs[column].scope; + + let text = this.translate(field, 'fields', scope); + + return $('
  • ') + .append( + $('') + .attr('role', 'button') + .attr('tabindex', '0') + .attr('data-action', 'toggleBoolColumn') + .attr('data-column', column) + .attr('data-id', id) + .append( + $('') + .addClass('check-icon fas fa-check pull-right') + .addClass(!value ? 'hidden' : '') + ) + .append( + $('
    ').text(text) + ) + ); + } + + /** @inheritDoc */ + addLinkHtml(id, name) { + if (this.isSearchMode()) { + return super.addLinkHtml(id, name); + } + + // Do not use the `html` method to avoid XSS. + + let $container = this.$el.find('.link-container'); + + let $el = $('
    ') + .addClass('form-inline clearfix') + .addClass('list-group-item link-with-role link-group-item-with-columns') + .addClass('link-' + id); + + let $remove = $('') + .attr('role', 'button') + .attr('tabindex', '0') + .attr('data-id', id) + .attr('data-action', 'clearLink') + .addClass('pull-right') + .append( + $('').addClass('fas fa-times') + ); + + let $name = $('
    ') + .addClass('link-item-name') + .text(name) + .append(' ') + + let $columnList = []; + let $liList = []; + + this.columnList.forEach(column => { + let value = (this.columns[id] || {})[column]; + + let type = this.columnsDefs[column].type; + + if (type === this.COLUMN_TYPE_ENUM) { + $columnList.push( + this.getJQSelect(column, id, value) + ); + + return; + } + + if (type === this.COLUMN_TYPE_VARCHAR) { + $columnList.push( + this.getJQInput(column, id, value) + ); + + return; + } + + if (type === this.COLUMN_TYPE_BOOL) { + $liList.push( + this.getJQLi(column, id, value) + ); + } + }); + + let $left = $('
    '); + let $right = $('
    '); + + $columnList.forEach($item => $left.append( + $('') + .addClass('link-item-column') + .addClass('link-item-column-' + $item.get(0).tagName.toLowerCase()) + .append($item) + )); + + if ($liList.length) { + let $ul = $('