From ad00a8cff8cc2392c3ca11fafd52a68187993682 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 3 Apr 2025 11:03:22 +0300 Subject: [PATCH] expanded layout item params --- .../Resources/i18n/en_US/DashletOptions.json | 4 + client/src/ui/multi-select.js | 13 +- .../fields/records/expanded-layout.js | 259 ++++++++++++++---- .../expanded-layout/modals/edit-item.js | 131 +++++++++ frontend/less/espo/misc/selectize/custom.less | 6 + 5 files changed, 357 insertions(+), 56 deletions(-) create mode 100644 client/src/views/dashlets/fields/records/expanded-layout/modals/edit-item.js diff --git a/application/Espo/Resources/i18n/en_US/DashletOptions.json b/application/Espo/Resources/i18n/en_US/DashletOptions.json index 9611e9ce36..6fa696cb8f 100644 --- a/application/Espo/Resources/i18n/en_US/DashletOptions.json +++ b/application/Espo/Resources/i18n/en_US/DashletOptions.json @@ -41,5 +41,9 @@ }, "tooltips": { "skipOwn": "Actions made by your user account won't be displayed." + }, + "otherFields": { + "soft": "Soft Color", + "small": "Small Font" } } diff --git a/client/src/ui/multi-select.js b/client/src/ui/multi-select.js index eaab75c8b3..0bb6475ca3 100644 --- a/client/src/ui/multi-select.js +++ b/client/src/ui/multi-select.js @@ -33,7 +33,7 @@ import Selectize from 'lib!selectize'; /** * @typedef module:ui/multi-select~Options * @type {Object} - * @property {{value: string, text: string, style?: string}[]} items + * @property {{value: string, text: string, style?: string, small?: boolean}[]} items * @property {string} [delimiter=':,:'] * @property {boolean} [restoreOnBackspace=false] * @property {boolean} [removeButton=true] @@ -165,11 +165,20 @@ const MultiSelect = { ...o, }), {}); + const classMap = {}; + + (options.items || []).forEach(it => { + if (it.small) { + classMap[it.value] = 'small'; + } + }); + selectizeOptions.render.item = (/** {text: string, value: string} */data, escape) => { const text = escape(data.text); const style = escape(styleMap[data.value] || ''); + const classString = escape(classMap[data.value] || ''); - return `
${text} ` + + return `
${text} ` + `×
`; }; diff --git a/client/src/views/dashlets/fields/records/expanded-layout.js b/client/src/views/dashlets/fields/records/expanded-layout.js index a74fd7b7a7..59dcb7ca45 100644 --- a/client/src/views/dashlets/fields/records/expanded-layout.js +++ b/client/src/views/dashlets/fields/records/expanded-layout.js @@ -28,44 +28,144 @@ import BaseFieldView from 'views/fields/base'; import MultiSelect from 'ui/multi-select'; +import ExpandedLayoutEditItemModalFieldView from 'views/dashlets/fields/records/expanded-layout/modals/edit-item'; class ExpandedLayoutDashletFieldView extends BaseFieldView { // language=Handlebars editTemplateContent = ` -
+
+ {{#each rowDataList}} +
+
+ +
+ {{#if hasEdit}} +
+ + +
+ {{/if}} +
+ {{/each}} +
` delimiter = ':,:' + /** + * @private + * @type {string} + */ + targetEntityType + + data() { + const rowList = this.getRowList(); + + const dataList = [...rowList, []].map((it, i) => ({ + index: i, + value: it.map(subIt => subIt.name).join(this.delimiter), + hasEdit: i < rowList.length, + itemList: it.map(subIt => ({ + name: subIt.name, + label: this.translate(subIt.name, 'fields', this.targetEntityType), + })), + })); + + return { + rowDataList: dataList, + } + } + setup() { this.addHandler('change', 'div[data-role="layoutRow"] input', () => { - this.trigger('change'); - this.reRender(); + setTimeout(() => { + this.trigger('change'); + this.reRender(); + }, 1); }); + + this.addActionHandler('editItem', (event, target) => this.editItem(target.dataset.name)); + + this.targetEntityType = this.model.get('entityType') || + this.getMetadata().get(['dashlets', this.dataObject.dashletName, 'entityType']); + } + + /** + * @private + * @return {Array.<{name: string, link?: boolean, soft?: boolean, small?: boolean}>[]} + */ + getRowList() { + return Espo.Utils.cloneDeep((this.model.get(this.name) || {}).rows || []); } afterRenderEdit() { - const containerElement = this.element.querySelector(`:scope > .layout-container`); - - let rowList = (this.model.get(this.name) || {}).rows || []; - - rowList = Espo.Utils.cloneDeep(rowList); + const rowList = Espo.Utils.cloneDeep(this.getRowList()); rowList.push([]); const fieldDataList = this.getFieldDataList(); rowList.forEach((row, i) => { - const rowElement = this.createRowElement(row, i); + const usedOtherList = []; + const usedList = []; - containerElement.append(rowElement); + rowList.forEach((it, j) => { + usedList.push(...it.map(it => it.name)); - const inputElement = rowElement.querySelector('input'); + if (j === i) { + return; + } + + usedOtherList.push(...it.map(it => it.name)); + }); + + const preparedList = fieldDataList + .filter(it => !usedOtherList.includes(it.value)) + .map(it => { + if (!usedList.includes(it.value)) { + return it; + } + + const itemData = this.getItemData(it.value) || {}; + + if (itemData.soft) { + it.style = 'soft'; + } + + if (itemData.small) { + it.small = true; + } + + return it; + }); + + const inputElement = this.element.querySelector(`input[data-index="${i.toString()}"]`); /** @type {module:ui/multi-select~Options} */ const multiSelectOptions = { - items: fieldDataList, + items: preparedList, delimiter: this.delimiter, matchAnyWord: this.matchAnyWord, draggable: true, @@ -75,34 +175,6 @@ class ExpandedLayoutDashletFieldView extends BaseFieldView { }); } - /** - * @private - * @param {Record[]} row - * @param {number} i - * @return {HTMLDivElement} - */ - createRowElement(row, i) { - row = row || []; - - const list = []; - - row.forEach(item => { - list.push(item.name); - }); - - const div = document.createElement('div'); - div.dataset.role = 'layoutRow'; - - const input = document.createElement('input'); - input.type = 'text'; - input.classList.add('row-' + i.toString()); - input.value = list.join(this.delimiter); - - div.append(input); - - return div; - } - /** * @private * @return {{value: string, text: string}[]} @@ -166,14 +238,70 @@ class ExpandedLayoutDashletFieldView extends BaseFieldView { return dataList; } - fetch() { - const value = { - rows: [], - }; + /** + * @private + * @param {string} name + */ + async editItem(name) { + const inputData = this.getItemData(name); - this.$el.find('input').each((i, el) => { + const view = new ExpandedLayoutEditItemModalFieldView({ + label: this.translate(name, 'fields', this.targetEntityType), + data: inputData, + onApply: data => this.applyItem(name, data), + }); + + await this.assignView('modal', view); + await view.render(); + } + + /** + * @private + * @param {string} name + * @return {{ + * soft: boolean, + * small: boolean, + * }} + */ + getItemData(name) { + /** + * @type {{ + * soft: boolean, + * small: boolean, + * }} + */ + let inputData; + + for (const row of this.getRowList()) { + for (const item of row) { + if (item.name === name) { + inputData = { + soft: item.soft || false, + small: item.small || false, + }; + } + } + } + + return inputData; + } + + fetch() { + const value = {rows: []}; + + /** @type {Record.} */ + const params = {}; + + for (const row of this.getRowList()) { + for (const item of row) { + params[item.name] = item; + } + } + + this.element.querySelectorAll('input').forEach(/** HTMLInputElement*/inputElement => { const row = []; - let list = ($(el).val() || '').split(this.delimiter); + + let list = inputElement.value.split(this.delimiter); if (list.length === 1 && list[0] === '') { list = []; @@ -183,24 +311,47 @@ class ExpandedLayoutDashletFieldView extends BaseFieldView { return; } - list.forEach(item => { - const o = {name: item}; + list.forEach(name => { + const item = {name: name}; - if (item === 'name') { - o.link = true; + if (name === 'name') { + item.link = true; } - row.push(o); + if (params[name]) { + item.soft = params[name].soft || false; + item.small = params[name].small || false; + } + + row.push(item); }); value.rows.push(row); }); - const data = {}; + return {[this.name]: value}; + } - data[this.name] = value; + /** + * @private + * @param {string} name + * @param {{soft: boolean, small: boolean}} data + */ + applyItem(name, data) { + const rowList = this.getRowList(); - return data; + for (const row of rowList) { + for (const item of row) { + if (item.name === name) { + item.soft = data.soft; + item.small = data.small; + } + } + } + + this.model.set(this.name, {rows: rowList}, {ui: true}); + + this.reRender(); } } diff --git a/client/src/views/dashlets/fields/records/expanded-layout/modals/edit-item.js b/client/src/views/dashlets/fields/records/expanded-layout/modals/edit-item.js new file mode 100644 index 0000000000..8a8d45c40b --- /dev/null +++ b/client/src/views/dashlets/fields/records/expanded-layout/modals/edit-item.js @@ -0,0 +1,131 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2025 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 . + * + * 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. + ************************************************************************/ + +import ModalView from 'views/modal'; +import EditForModalRecordView from 'views/record/edit-for-modal'; +import Model from 'model'; +import BoolFieldView from 'views/fields/bool'; + +export default class ExpandedLayoutEditItemModalFieldView extends ModalView { + + // language=Handlebars + templateContent = ` +
{{{record}}}
+ ` + + /** + * @private + * @type {EditForModalRecordView} + */ + recordView + + /** + * @private + * @type {Model} + */ + formModel + + /** + * @param {{ + * onApply: function({soft: boolean, small: boolean}), + * label: string, + * data: { + * soft: boolean, + * small: boolean, + * }, + * }} options + */ + constructor(options) { + super(); + + this.options = options; + } + + setup() { + this.headerText = this.translate('Edit') + ' · ' + this.options.label; + + this.formModel = new Model(); + this.formModel.setMultiple({...this.options.data}); + + this.recordView = new EditForModalRecordView({ + model: this.formModel, + detailLayout: [ + { + rows: [ + [ + { + view: new BoolFieldView({ + name: 'soft', + labelText: this.translate('soft', 'otherFields', 'DashletOptions'), + }), + }, + { + view: new BoolFieldView({ + name: 'small', + labelText: this.translate('small', 'otherFields', 'DashletOptions'), + }), + }, + ], + ] + } + ] + }); + + this.assignView('record', this.recordView); + + this.buttonList = [ + { + name: 'apply', + style: 'danger', + label: 'Apply', + onClick: () => this.actionApply(), + }, + { + name: 'cancel', + label: 'Cancel', + onClick: () => this.actionClose(), + }, + ] + } + + /** + * @private + */ + actionApply() { + if (this.recordView.validate()) { + return; + } + + this.options.onApply({ + soft: this.formModel.attributes.soft, + small: this.formModel.attributes.small, + }); + + this.close(); + } +} diff --git a/frontend/less/espo/misc/selectize/custom.less b/frontend/less/espo/misc/selectize/custom.less index f2d2d2defd..13cf0d8d1a 100644 --- a/frontend/less/espo/misc/selectize/custom.less +++ b/frontend/less/espo/misc/selectize/custom.less @@ -105,6 +105,12 @@ color: var(--state-success-text); } } + + &.soft { + > span { + color: var(--gray-soft); + } + } } } }