expanded layout item params
This commit is contained in:
@@ -41,5 +41,9 @@
|
||||
},
|
||||
"tooltips": {
|
||||
"skipOwn": "Actions made by your user account won't be displayed."
|
||||
},
|
||||
"otherFields": {
|
||||
"soft": "Soft Color",
|
||||
"small": "Small Font"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 `<div class="item ${style}"><span>${text}</span> ` +
|
||||
return `<div class="item ${style}"><span class="${classString}">${text}</span> ` +
|
||||
`<a href="javascript:" class="remove">×</a></div>`;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 = `
|
||||
<div class="layout-container"></div>
|
||||
<div class="layout-container">
|
||||
{{#each rowDataList}}
|
||||
<div data-role="layoutRow">
|
||||
<div
|
||||
style="display: inline-block; width: calc(100% - var(--40px));"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="{{value}}"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
</div>
|
||||
{{#if hasEdit}}
|
||||
<div class="btn-group pull-right">
|
||||
<button
|
||||
class="btn btn-text dropdown-toggle"
|
||||
data-toggle="dropdown"
|
||||
><span class="caret"></span></button>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li class="dropdown-header">{{translate 'Edit'}}</li>
|
||||
{{#each itemList}}
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-action="editItem"
|
||||
data-name="{{name}}"
|
||||
>{{label}}</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
`
|
||||
|
||||
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.<string, 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <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.
|
||||
************************************************************************/
|
||||
|
||||
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 = `
|
||||
<div class="record-container no-side-margin">{{{record}}}</div>
|
||||
`
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,12 @@
|
||||
color: var(--state-success-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.soft {
|
||||
> span {
|
||||
color: var(--gray-soft);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user