snapshot of https://github.com/espocrm/espocrm.git (master, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Test / Test on PHP 8.3 (push) Failing after 5s
Test / Test on PHP 8.4 (push) Failing after 4s
Test / Test on PHP 8.5 (push) Failing after 3s
TypeScript static analysis / TypeScript check (push) Failing after 14s
Test Integration on MySQL / Test on PHP 8.3 (push) Failing after 1s
Test Integration on MySQL / Test on PHP 8.5 (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 fix (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 master (push) Failing after 1s
Some checks failed
Test / Test on PHP 8.3 (push) Failing after 5s
Test / Test on PHP 8.4 (push) Failing after 4s
Test / Test on PHP 8.5 (push) Failing after 3s
TypeScript static analysis / TypeScript check (push) Failing after 14s
Test Integration on MySQL / Test on PHP 8.3 (push) Failing after 1s
Test Integration on MySQL / Test on PHP 8.5 (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 fix (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 master (push) Failing after 1s
This commit is contained in:
109
client/src/helpers/record/create-related.js
Normal file
109
client/src/helpers/record/create-related.js
Normal file
@@ -0,0 +1,109 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* 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 {inject} from 'di';
|
||||
import Metadata from 'metadata';
|
||||
import RecordModal from 'helpers/record-modal';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CreateRelatedHelper {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Metadata}
|
||||
*/
|
||||
@inject(Metadata)
|
||||
metadata
|
||||
|
||||
/**
|
||||
* @param {import('view').default} view
|
||||
*/
|
||||
constructor(view) {
|
||||
/** @private */
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('model').default} model
|
||||
* @param {string} link
|
||||
* @param {{
|
||||
* focusForCreate?: boolean,
|
||||
* afterSave: function(import('model').default),
|
||||
* }} [options]
|
||||
* @return {Promise<import('views/modals/edit').default>}
|
||||
*/
|
||||
async process(model, link, options = {}) {
|
||||
const scope = model.defs['links'][link].entity;
|
||||
const foreignLink = model.defs['links'][link].foreign;
|
||||
|
||||
/** @type {Record} */
|
||||
const panelDefs = this.metadata.get(`clientDefs.${model.entityType}.relationshipPanels.${link}`) || {};
|
||||
|
||||
const attributeMap = panelDefs.createAttributeMap || {};
|
||||
const handler = panelDefs.createHandler;
|
||||
|
||||
let attributes = {};
|
||||
|
||||
Object.keys(attributeMap).forEach(attr => attributes[attributeMap[attr]] = model.get(attr));
|
||||
|
||||
if (handler) {
|
||||
const Handler = await Espo.loader.requirePromise(handler);
|
||||
/** @type {import('contracts/relation').CreateRelatedHandler} */
|
||||
const handlerObj = new Handler(this.view.getHelper(), {link: link});
|
||||
|
||||
const additionalAttributes = await handlerObj.getAttributes(model, link);
|
||||
|
||||
attributes = {...attributes, ...additionalAttributes};
|
||||
}
|
||||
|
||||
const helper = new RecordModal();
|
||||
|
||||
return await helper.showCreate(this.view, {
|
||||
entityType: scope,
|
||||
relate: {
|
||||
model: model,
|
||||
link: foreignLink,
|
||||
},
|
||||
attributes: attributes,
|
||||
focusForCreate: options.focusForCreate,
|
||||
afterSave: m => {
|
||||
if (options.afterSave) {
|
||||
options.afterSave(m);
|
||||
}
|
||||
|
||||
model.trigger(`update-related:${link}`);
|
||||
model.trigger('after:relate');
|
||||
model.trigger(`after:relate:${link}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default CreateRelatedHelper;
|
||||
376
client/src/helpers/record/detail/layout-converter.js
Normal file
376
client/src/helpers/record/detail/layout-converter.js
Normal file
@@ -0,0 +1,376 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* 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 {inject} from 'di';
|
||||
import Language from 'language';
|
||||
import ViewHelper from 'view-helper';
|
||||
import FieldManager from 'field-manager';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class LayoutConverter {
|
||||
|
||||
/**
|
||||
* @type {Language}
|
||||
* @private
|
||||
*/
|
||||
@inject(Language)
|
||||
language
|
||||
|
||||
/**
|
||||
* @type {ViewHelper}
|
||||
* @private
|
||||
*/
|
||||
@inject(ViewHelper)
|
||||
viewHelper
|
||||
|
||||
/**
|
||||
* @type {FieldManager}
|
||||
* @private
|
||||
*/
|
||||
@inject(FieldManager)
|
||||
fieldManager
|
||||
|
||||
/**
|
||||
* Convert a detail layout to an internal layout.
|
||||
*
|
||||
* @param {import('views/record/detail').PanelDefs[]} simplifiedLayout A detail layout.
|
||||
* @param {{
|
||||
* selector: string,
|
||||
* id: string,
|
||||
* entityType: string | null,
|
||||
* model: import('model').default,
|
||||
* panelFieldListMap: Record<string, string[]>,
|
||||
* middlePanelDefs: Record<string, any>,
|
||||
* middlePanelDefsList: Record<string, any>[],
|
||||
* underShowMoreDetailPanelList: string[],
|
||||
* dynamicLogic: import('dynamic-logic').default | null,
|
||||
* dynamicLogicDefs: import('dynamic-logic').Defs | null,
|
||||
* recordHelper: import('view-record-helper').default,
|
||||
* hidePanel: (name: string) => void,
|
||||
* validateField: (name: string) => boolean,
|
||||
* readOnly: boolean,
|
||||
* readOnlyLocked: boolean,
|
||||
* inlineEditDisabled: boolean,
|
||||
* dataObject: Record<string, any>,
|
||||
* fieldsMode: 'detail' | 'edit' | 'list',
|
||||
* }} options
|
||||
* @return {Record<string, any>[]}
|
||||
*/
|
||||
convert(simplifiedLayout, options) {
|
||||
const layout = [];
|
||||
const el = options.selector || '#' + options.id;
|
||||
|
||||
const panelFieldListMap = options.panelFieldListMap;
|
||||
const middlePanelDefs = options.middlePanelDefs;
|
||||
const middlePanelDefsList = options.middlePanelDefsList;
|
||||
const underShowMoreDetailPanelList = options.underShowMoreDetailPanelList;
|
||||
|
||||
const recordHelper = options.recordHelper;
|
||||
const dynamicLogic = options.dynamicLogic;
|
||||
const dynamicLogicDefs = options.dynamicLogicDefs;
|
||||
const entityType = options.entityType;
|
||||
|
||||
let tabNumber = -1;
|
||||
|
||||
for (let p = 0; p < simplifiedLayout.length; p++) {
|
||||
const item = simplifiedLayout[p];
|
||||
|
||||
const panel = {};
|
||||
|
||||
const tabBreak = item.tabBreak || p === 0;
|
||||
|
||||
if (tabBreak) {
|
||||
tabNumber++;
|
||||
}
|
||||
|
||||
if ('customLabel' in item) {
|
||||
panel.label = item.customLabel;
|
||||
|
||||
if (panel.label) {
|
||||
panel.label = this.language.translate(panel.label, 'panelCustomLabels', entityType);
|
||||
}
|
||||
} else {
|
||||
panel.label = item.label || null;
|
||||
|
||||
if (panel.label) {
|
||||
panel.label = panel.label[0] === '$' ?
|
||||
this.language.translate(panel.label.substring(1), 'panels', entityType) :
|
||||
this.language.translate(panel.label, 'labels', entityType);
|
||||
}
|
||||
}
|
||||
|
||||
panel.name = item.name || 'panel-' + p.toString();
|
||||
panel.style = item.style || 'default';
|
||||
panel.rows = [];
|
||||
panel.tabNumber = tabNumber;
|
||||
panel.noteText = item.noteText;
|
||||
panel.noteStyle = item.noteStyle || 'info';
|
||||
|
||||
if (panel.noteText) {
|
||||
if (panel.noteText.startsWith('$') && !panel.noteText.includes(' ')) {
|
||||
const label = panel.noteText.substring(1);
|
||||
|
||||
panel.noteText = this.language.translate(label, 'panelNotes', entityType);
|
||||
}
|
||||
|
||||
panel.noteText = this.viewHelper.transformMarkdownText(panel.noteText);
|
||||
}
|
||||
|
||||
middlePanelDefs[panel.name] = {
|
||||
name: panel.name,
|
||||
style: panel.style,
|
||||
tabNumber: panel.tabNumber,
|
||||
tabBreak: tabBreak,
|
||||
tabLabel: item.tabLabel,
|
||||
};
|
||||
|
||||
middlePanelDefsList.push(middlePanelDefs[panel.name]);
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
if (item.dynamicLogicVisible && dynamicLogic) {
|
||||
dynamicLogic.addPanelVisibleCondition(panel.name, item.dynamicLogicVisible);
|
||||
}
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
if (item.dynamicLogicStyled && dynamicLogic) {
|
||||
dynamicLogic.addPanelStyledCondition(panel.name, item.dynamicLogicStyled);
|
||||
}
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
if (item.hidden && tabNumber === 0) {
|
||||
panel.hidden = true;
|
||||
|
||||
options.hidePanel(panel.name);
|
||||
|
||||
underShowMoreDetailPanelList.push(panel.name);
|
||||
}
|
||||
|
||||
let lType = 'rows';
|
||||
|
||||
if (item.columns) {
|
||||
lType = 'columns';
|
||||
|
||||
panel.columns = [];
|
||||
}
|
||||
|
||||
if (panel.name) {
|
||||
panelFieldListMap[panel.name] = [];
|
||||
}
|
||||
|
||||
for (const [i, itemI] of item[lType].entries()) {
|
||||
const row = [];
|
||||
|
||||
for (const cellDefs of itemI) {
|
||||
if (cellDefs === false) {
|
||||
row.push(false);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let view = cellDefs.view;
|
||||
let name = cellDefs.name;
|
||||
|
||||
if (!name && view && typeof view === 'object') {
|
||||
name = view.name;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
console.warn(`No 'name' specified in detail layout cell.`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let selector;
|
||||
|
||||
if (view && typeof view === 'object') {
|
||||
view.model = options.model;
|
||||
view.mode = options.fieldsMode;
|
||||
|
||||
if (options.readOnly) {
|
||||
view.setReadOnly();
|
||||
}
|
||||
|
||||
selector = `.field[data-name="${name}"]`;
|
||||
}
|
||||
|
||||
if (panel.name) {
|
||||
panelFieldListMap[panel.name].push(name);
|
||||
}
|
||||
|
||||
const type = cellDefs.type || options.model.getFieldType(name) || 'base';
|
||||
|
||||
view = view ||
|
||||
options.model.getFieldParam(name, 'view') ||
|
||||
this.fieldManager.getViewName(type);
|
||||
|
||||
const o = {
|
||||
fullSelector: `${el} .middle .field[data-name="${name}"]`,
|
||||
defs: {
|
||||
name: name,
|
||||
params: cellDefs.params || {},
|
||||
},
|
||||
mode: options.fieldsMode,
|
||||
};
|
||||
|
||||
if (options.readOnly) {
|
||||
o.readOnly = true;
|
||||
}
|
||||
|
||||
if (cellDefs.readOnly) {
|
||||
o.readOnly = true;
|
||||
o.readOnlyLocked = true;
|
||||
}
|
||||
|
||||
if (options.readOnlyLocked) {
|
||||
o.readOnlyLocked = true;
|
||||
}
|
||||
|
||||
if (options.inlineEditDisabled || cellDefs.inlineEditDisabled) {
|
||||
o.inlineEditDisabled = true;
|
||||
}
|
||||
|
||||
// noinspection JSUnresolvedReference
|
||||
let fullWidth = cellDefs.fullWidth || false;
|
||||
|
||||
if (!fullWidth) {
|
||||
if (item[lType][i].length === 1) {
|
||||
fullWidth = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (recordHelper.getFieldStateParam(name, 'hidden')) {
|
||||
o.disabled = true;
|
||||
}
|
||||
|
||||
if (recordHelper.getFieldStateParam(name, 'hiddenLocked')) {
|
||||
o.disabledLocked = true;
|
||||
}
|
||||
|
||||
if (recordHelper.getFieldStateParam(name, 'readOnly')) {
|
||||
o.readOnly = true;
|
||||
}
|
||||
|
||||
if (!o.readOnlyLocked && recordHelper.getFieldStateParam(name, 'readOnlyLocked')) {
|
||||
o.readOnlyLocked = true;
|
||||
}
|
||||
|
||||
if (recordHelper.getFieldStateParam(name, 'required') !== null) {
|
||||
o.defs.params = o.defs.params || {};
|
||||
o.defs.params.required = recordHelper.getFieldStateParam(name, 'required');
|
||||
}
|
||||
|
||||
if (recordHelper.hasFieldOptionList(name)) {
|
||||
o.customOptionList = recordHelper.getFieldOptionList(name);
|
||||
}
|
||||
|
||||
o.validateCallback = () => options.validateField(name);
|
||||
|
||||
o.recordHelper = recordHelper;
|
||||
o.dataObject = options.dataObject;
|
||||
|
||||
if (cellDefs.options) {
|
||||
for (const optionName in cellDefs.options) {
|
||||
if (typeof o[optionName] !== 'undefined') {
|
||||
continue;
|
||||
}
|
||||
|
||||
o[optionName] = cellDefs.options[optionName];
|
||||
}
|
||||
}
|
||||
|
||||
if (dynamicLogicDefs?.cascadingFields?.[name]) {
|
||||
o.cascadingLogic = dynamicLogicDefs?.cascadingFields?.[name];
|
||||
}
|
||||
|
||||
const cell = {
|
||||
name: `${name}Field`,
|
||||
view: view,
|
||||
field: name,
|
||||
fullSelector: `${el} .middle .field[data-name="${name}"]`,
|
||||
fullWidth: fullWidth,
|
||||
options: o,
|
||||
};
|
||||
|
||||
if (selector) {
|
||||
cell.selector = selector;
|
||||
}
|
||||
|
||||
if ('labelText' in cellDefs) {
|
||||
o.labelText = cellDefs.labelText;
|
||||
cell.customLabel = cellDefs.labelText;
|
||||
} else if (cellDefs.labelTranslation) {
|
||||
o.labelText = this.language.translatePath(cellDefs.labelTranslation);
|
||||
cell.customLabel = o.labelText;
|
||||
}
|
||||
|
||||
if ('customLabel' in cellDefs) {
|
||||
cell.customLabel = cellDefs.customLabel;
|
||||
}
|
||||
|
||||
if ('label' in cellDefs) {
|
||||
cell.label = cellDefs.label;
|
||||
}
|
||||
|
||||
if (
|
||||
view &&
|
||||
typeof view === 'object' &&
|
||||
!cell.customLabel &&
|
||||
!cell.label &&
|
||||
view.getLabelText()
|
||||
) {
|
||||
cell.customLabel = view.getLabelText();
|
||||
}
|
||||
|
||||
if ('customCode' in cellDefs) {
|
||||
cell.customCode = cellDefs.customCode;
|
||||
}
|
||||
|
||||
if ('noLabel' in cellDefs) {
|
||||
cell.noLabel = cellDefs.noLabel;
|
||||
}
|
||||
|
||||
if ('span' in cellDefs) {
|
||||
cell.span = cellDefs.span;
|
||||
}
|
||||
|
||||
row.push(cell);
|
||||
}
|
||||
|
||||
panel[lType].push(row);
|
||||
}
|
||||
|
||||
layout.push(panel);
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
||||
export default LayoutConverter;
|
||||
237
client/src/helpers/record/list/column-resize.js
Normal file
237
client/src/helpers/record/list/column-resize.js
Normal file
@@ -0,0 +1,237 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* 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.
|
||||
************************************************************************/
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export default class ListColumnResizeHelper {
|
||||
|
||||
/**
|
||||
* @type {{
|
||||
* startX: number,
|
||||
* startWidth: number,
|
||||
* thElement: HTMLTableCellElement,
|
||||
* name: string,
|
||||
* inPx: boolean,
|
||||
* onRight: boolean,
|
||||
* newWidth: number|null,
|
||||
* thElements: HTMLTableCellElement[],
|
||||
* }}
|
||||
* @private
|
||||
*/
|
||||
item
|
||||
|
||||
/**
|
||||
* A min width in pixels.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
minWidth = 30
|
||||
|
||||
static selector = 'table > thead > tr > th > .column-resizer';
|
||||
|
||||
/**
|
||||
* @param {import('views/record/list-base').default} view
|
||||
* @param {import('helpers/list/settings').default} helper
|
||||
*/
|
||||
constructor(view, helper) {
|
||||
/** @private */
|
||||
this.view = view;
|
||||
/** @private */
|
||||
this.helper = helper;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
this.fontSizeFactor = view.getThemeManager().getFontSizeFactor();
|
||||
|
||||
this.onPointerUpBind = this.onPointerUp.bind(this);
|
||||
this.onPointerMoveBind = this.onPointerMove.bind(this);
|
||||
|
||||
view.addHandler('pointerdown', ListColumnResizeHelper.selector, (/** PointerEvent */e, target) => {
|
||||
this.onPointerDown(e, target);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
onPointerDown(event, target) {
|
||||
if (!event.isPrimary) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.startResizeInit(event, target)
|
||||
|
||||
window.addEventListener('pointerup', this.onPointerUpBind);
|
||||
window.addEventListener('pointermove', this.onPointerMoveBind);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {PointerEvent} event
|
||||
* @param {HTMLElement} target
|
||||
*/
|
||||
startResizeInit(event, target) {
|
||||
const th = /** @type {HTMLTableCellElement} */target.parentNode;
|
||||
|
||||
const thElements = [...th.parentNode.querySelectorAll(':scope > th.field-header-cell')]
|
||||
.filter(it => !it.style.width);
|
||||
|
||||
this.item = {
|
||||
startX: event.clientX,
|
||||
startWidth: th.clientWidth,
|
||||
thElement: th,
|
||||
name: th.dataset.name,
|
||||
inPx: th.style.width && th.style.width.endsWith('px'),
|
||||
onRight: target.classList.contains('column-resizer-right'),
|
||||
newWidth: null,
|
||||
thElements: thElements,
|
||||
};
|
||||
|
||||
document.body.style.cursor = 'col-resize';
|
||||
|
||||
const trElement = this.item.thElement.closest('tr');
|
||||
|
||||
trElement.classList.add('being-column-resized');
|
||||
this.item.thElement.classList.add('being-resized');
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {number} width
|
||||
*/
|
||||
isWidthOk(width) {
|
||||
if (width < this.minWidth * this.fontSizeFactor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const th of this.item.thElements) {
|
||||
if (th.style.width) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (th.clientWidth < this.minWidth * this.fontSizeFactor) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {PointerEvent} event
|
||||
*/
|
||||
onPointerMove(event) {
|
||||
let diff = event.clientX - this.item.startX;
|
||||
|
||||
if (!this.item.onRight) {
|
||||
diff *= -1;
|
||||
}
|
||||
|
||||
const width = this.item.startWidth + diff;
|
||||
|
||||
if (!this.isWidthOk(width)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousWidth = this.item.newWidth;
|
||||
const previousStyleWidth = this.item.thElement.style.width;
|
||||
|
||||
this.item.newWidth = width;
|
||||
this.item.thElement.style.width = width.toString() + 'px';
|
||||
|
||||
if (!this.isWidthOk(width)) {
|
||||
if (previousWidth) {
|
||||
this.item.newWidth = previousWidth;
|
||||
}
|
||||
|
||||
this.item.thElement.style.width = previousStyleWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onPointerUp() {
|
||||
window.removeEventListener('pointermove', this.onPointerMoveBind);
|
||||
window.removeEventListener('pointerup', this.onPointerUpBind);
|
||||
document.body.style.cursor = '';
|
||||
|
||||
const width = this.item.newWidth;
|
||||
|
||||
if (width === null) {
|
||||
this.disableResizingState();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let unit = 'px';
|
||||
let value = width;
|
||||
|
||||
if (!this.item.inPx) {
|
||||
const tableElement = this.item.thElement.closest('table');
|
||||
|
||||
const tableWidth = tableElement.clientWidth;
|
||||
|
||||
const factor = Math.pow(10, 4);
|
||||
const widthPercents = width / tableWidth;
|
||||
const widthPercentsRounded = Math.floor(factor * widthPercents * 100) / factor;
|
||||
|
||||
this.item.thElement.style.width = widthPercentsRounded.toString() + '%';
|
||||
|
||||
unit = '%';
|
||||
value = widthPercentsRounded;
|
||||
}
|
||||
|
||||
if (this.item.inPx) {
|
||||
value = value / this.fontSizeFactor;
|
||||
}
|
||||
|
||||
this.helper.storeColumnWidth(this.item.name, {value: value, unit: unit});
|
||||
|
||||
this.disableResizingState();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
disableResizingState() {
|
||||
const trElement = this.item.thElement.closest('tr')
|
||||
|
||||
trElement.classList.remove('being-column-resized');
|
||||
this.item.thElement.classList.remove('being-resized');
|
||||
|
||||
this.item = undefined;
|
||||
}
|
||||
}
|
||||
214
client/src/helpers/record/list/column-width-control.js
Normal file
214
client/src/helpers/record/list/column-width-control.js
Normal file
@@ -0,0 +1,214 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* 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.
|
||||
************************************************************************/
|
||||
|
||||
export default class ListColumnWidthControlHelper {
|
||||
|
||||
/**
|
||||
* A min width in pixels.
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
minWidth = 30
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* view?: import('views/record/list-base').default,
|
||||
* helper: import('helpers/list/settings').default,
|
||||
* layoutProvider?: function(): {
|
||||
* name: string,
|
||||
* width?: number,
|
||||
* widthPx?: number,
|
||||
* hidden?: boolean,
|
||||
* }[]
|
||||
* }} options
|
||||
*/
|
||||
constructor(options) {
|
||||
/** @private */
|
||||
this.view = options.view;
|
||||
/** @private */
|
||||
this.helper = options.helper;
|
||||
/** @private */
|
||||
this.layoutProvider = options.layoutProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust widths.
|
||||
*
|
||||
* @param {{
|
||||
* tableWidth?: number,
|
||||
* staticWidth?: number,
|
||||
* }} [options]
|
||||
* @return {boolean}
|
||||
*/
|
||||
adjust(options = {}) {
|
||||
let tableWidthData;
|
||||
|
||||
if (options.tableWidth === undefined || options.staticWidth === undefined) {
|
||||
tableWidthData = this.getTableWidths();
|
||||
}
|
||||
|
||||
const tableWidth = options.tableWidth === undefined ? tableWidthData.table : options.tableWidth;
|
||||
const staticWidth = options.staticWidth === undefined ? tableWidthData.static : options.staticWidth;
|
||||
|
||||
const widthMap = this.helper.getColumnWidthMap();
|
||||
|
||||
/**
|
||||
* @type {{
|
||||
* name: string,
|
||||
* width: {
|
||||
* value: number,
|
||||
* unit: 'px'|'%',
|
||||
* }|null,
|
||||
* isCustom: boolean,
|
||||
* widthPx: number|null,
|
||||
* }[]}
|
||||
*/
|
||||
const list = this.layoutProvider()
|
||||
.filter(it => !this.helper.isColumnHidden(it.name, it.hidden))
|
||||
.map(it => {
|
||||
let widthItem = widthMap[it.name];
|
||||
|
||||
const isCustom = !!widthItem;
|
||||
|
||||
if (!widthItem) {
|
||||
widthItem = null;
|
||||
|
||||
if (it.width) {
|
||||
widthItem = {value: it.width, unit: '%'};
|
||||
} else if (it.widthPx) {
|
||||
widthItem = {value: it.widthPx, unit: 'px'};
|
||||
}
|
||||
}
|
||||
|
||||
let widthPx = null;
|
||||
|
||||
if (widthItem) {
|
||||
if (widthItem.unit === 'px') {
|
||||
widthPx = widthItem.value;
|
||||
} else {
|
||||
widthPx = tableWidth * (widthItem.value / 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: it.name,
|
||||
width: widthItem,
|
||||
isCustom: isCustom,
|
||||
widthPx: widthPx,
|
||||
};
|
||||
});
|
||||
|
||||
const flexColumnCount = list.filter(it => !it.width).length;
|
||||
const extraWidth = flexColumnCount * this.minWidth;
|
||||
|
||||
let sumWidth = 0;
|
||||
|
||||
list.filter(it => it.widthPx)
|
||||
.forEach(it => sumWidth += it.widthPx);
|
||||
|
||||
if (tableWidth - extraWidth - staticWidth - sumWidth >= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const listSorted = list
|
||||
.filter(it => it.widthPx && it.width)
|
||||
.sort((a, b) => b.widthPx - a.widthPx);
|
||||
|
||||
if (!listSorted.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const item = listSorted[0];
|
||||
|
||||
const reduceWidthPx = 10;
|
||||
|
||||
if (item.widthPx < reduceWidthPx) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @type {{value: number, unit: 'px'|'%'}} */
|
||||
let newWidth;
|
||||
|
||||
if (item.width.unit === 'px') {
|
||||
newWidth = {
|
||||
value: item.width.value - reduceWidthPx,
|
||||
unit: 'px',
|
||||
};
|
||||
} else {
|
||||
const factor = Math.pow(10, 4);
|
||||
const reducePercent = Math.floor(factor * (reduceWidthPx / tableWidth) * 100) / factor;
|
||||
|
||||
newWidth = {
|
||||
value: item.width.value - reducePercent,
|
||||
unit: '%',
|
||||
}
|
||||
}
|
||||
|
||||
const map = this.helper.getColumnWidthMap();
|
||||
map[item.name] = newWidth;
|
||||
|
||||
this.helper.storeColumnWidthMap(map);
|
||||
|
||||
this.adjust({tableWidth, staticWidth});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {{
|
||||
* table: number,
|
||||
* static: number,
|
||||
* }|null}
|
||||
*/
|
||||
getTableWidths() {
|
||||
const tableElement = this.view.element.querySelector('.list > table');
|
||||
|
||||
if (!tableElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tableWidth = tableElement.clientWidth;
|
||||
let staticWidth = 0;
|
||||
|
||||
tableElement.querySelectorAll(':scope > thead > tr > th').forEach(th => {
|
||||
if (
|
||||
!th.classList.contains('field-header-cell') ||
|
||||
th.classList.contains('action-cell')
|
||||
) {
|
||||
staticWidth += th.clientWidth;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
table: tableWidth,
|
||||
static: staticWidth,
|
||||
};
|
||||
}
|
||||
}
|
||||
149
client/src/helpers/record/misc/sticky-bar.js
Normal file
149
client/src/helpers/record/misc/sticky-bar.js
Normal file
@@ -0,0 +1,149 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* 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 $ from 'jquery';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class StickyBarHelper {
|
||||
|
||||
|
||||
/**
|
||||
* @param {import('views/record/detail').default} view
|
||||
* @param {string} stickButtonsFormBottomSelector
|
||||
* @param {boolean} stickButtonsContainerAllTheWay
|
||||
* @param {string} numId
|
||||
*/
|
||||
constructor(view, stickButtonsFormBottomSelector, stickButtonsContainerAllTheWay, numId) {
|
||||
this.view = view;
|
||||
this.stickButtonsFormBottomSelector = stickButtonsFormBottomSelector;
|
||||
this.stickButtonsContainerAllTheWay = stickButtonsContainerAllTheWay;
|
||||
this.numId = numId;
|
||||
|
||||
this.themeManager = view.getThemeManager();
|
||||
this.$el = view.$el;
|
||||
}
|
||||
|
||||
init() {
|
||||
const $containers = this.$el.find('.detail-button-container');
|
||||
const $container = this.$el.find('.detail-button-container.record-buttons');
|
||||
|
||||
if (!$container.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const navbarHeight = this.themeManager.getParam('navbarHeight') * this.themeManager.getFontSizeFactor();
|
||||
const screenWidthXs = this.themeManager.getParam('screenWidthXs');
|
||||
|
||||
const isSmallScreen = $(window.document).width() < screenWidthXs;
|
||||
|
||||
const getOffsetTop = (/** JQuery */$element) => {
|
||||
let element = /** @type {HTMLElement} */$element.get(0);
|
||||
|
||||
let value = 0;
|
||||
|
||||
while (element) {
|
||||
value += !isNaN(element.offsetTop) ? element.offsetTop : 0;
|
||||
|
||||
element = element.offsetParent;
|
||||
}
|
||||
|
||||
if (isSmallScreen) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value - navbarHeight;
|
||||
};
|
||||
|
||||
let stickTop = getOffsetTop($container);
|
||||
const blockHeight = $container.outerHeight();
|
||||
|
||||
stickTop -= 5; // padding;
|
||||
|
||||
const $block = $('<div>')
|
||||
.css('height', blockHeight + 'px')
|
||||
.html(' ')
|
||||
.hide()
|
||||
.insertAfter($container);
|
||||
|
||||
let $middle = this.view.getMiddleView().$el;
|
||||
const $window = $(window);
|
||||
const $navbarRight = $('#navbar .navbar-right');
|
||||
|
||||
if (this.stickButtonsFormBottomSelector) {
|
||||
const $bottom = this.$el.find(this.stickButtonsFormBottomSelector);
|
||||
|
||||
if ($bottom.length) {
|
||||
$middle = $bottom;
|
||||
}
|
||||
}
|
||||
|
||||
$window.off('scroll.detail-' + this.numId);
|
||||
|
||||
$window.on('scroll.detail-' + this.numId, () => {
|
||||
const edge = $middle.position().top + $middle.outerHeight(false) - blockHeight;
|
||||
const scrollTop = $window.scrollTop();
|
||||
|
||||
if (scrollTop >= edge && !this.stickButtonsContainerAllTheWay) {
|
||||
$containers.hide();
|
||||
$navbarRight.removeClass('has-sticked-bar');
|
||||
$block.show();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSmallScreen && $('#navbar .navbar-body').hasClass('in')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollTop > stickTop) {
|
||||
if (!$containers.hasClass('stick-sub')) {
|
||||
$containers.addClass('stick-sub');
|
||||
$block.show();
|
||||
}
|
||||
|
||||
$navbarRight.addClass('has-sticked-bar');
|
||||
|
||||
$containers.show();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($containers.hasClass('stick-sub')) {
|
||||
$containers.removeClass('stick-sub');
|
||||
$navbarRight.removeClass('has-sticked-bar');
|
||||
$block.hide();
|
||||
}
|
||||
|
||||
$containers.show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default StickyBarHelper;
|
||||
229
client/src/helpers/record/select-related.js
Normal file
229
client/src/helpers/record/select-related.js
Normal file
@@ -0,0 +1,229 @@
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM – Open Source CRM application.
|
||||
* Copyright (C) 2014-2026 EspoCRM, Inc.
|
||||
* 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 {inject} from 'di';
|
||||
import Metadata from 'metadata';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SelectRelatedHelper {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Metadata}
|
||||
*/
|
||||
@inject(Metadata)
|
||||
metadata
|
||||
|
||||
/**
|
||||
* @param {import('view').default} view
|
||||
*/
|
||||
constructor(view) {
|
||||
/** @private */
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('model').default} model
|
||||
* @param {string} link
|
||||
* @param {{
|
||||
* foreignEntityType?: string,
|
||||
* massSelect?: boolean,
|
||||
* primaryFilterName?: string,
|
||||
* boolFilterList?: string[]|string,
|
||||
* viewKey?: string,
|
||||
* hasCreate?: boolean,
|
||||
* onCreate?: function(): void,
|
||||
* }} options
|
||||
*/
|
||||
process(model, link, options = {}) {
|
||||
if (!options.foreignEntityType && !model.defs['links'][link]) {
|
||||
throw new Error(`Link ${link} does not exist.`);
|
||||
}
|
||||
|
||||
const scope = options.foreignEntityType || model.defs['links'][link].entity;
|
||||
|
||||
/** @var {Object.<string, *>} */
|
||||
const panelDefs = this.metadata.get(['clientDefs', model.entityType, 'relationshipPanels', link]) || {};
|
||||
|
||||
const massRelateEnabled = options.massSelect || panelDefs.massSelect;
|
||||
|
||||
let advanced = {};
|
||||
|
||||
const foreignLink = model.getLinkParam(link, 'foreign');
|
||||
|
||||
if (foreignLink && scope) {
|
||||
// Select only records not related with any.
|
||||
const foreignLinkType = this.metadata.get(['entityDefs', scope, 'links', foreignLink, 'type']);
|
||||
const foreignLinkFieldType = this.metadata.get(['entityDefs', scope, 'fields', foreignLink, 'type']);
|
||||
|
||||
if (
|
||||
['belongsTo', 'belongsToParent'].includes(foreignLinkType) &&
|
||||
foreignLinkFieldType &&
|
||||
!advanced[foreignLink] &&
|
||||
['link', 'linkParent'].includes(foreignLinkFieldType)
|
||||
) {
|
||||
advanced[foreignLink] = {
|
||||
type: 'isNull',
|
||||
attribute: foreignLink + 'Id',
|
||||
data: {
|
||||
type: 'isEmpty',
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let primaryFilterName = options.primaryFilterName || null;
|
||||
|
||||
if (typeof primaryFilterName === 'function') {
|
||||
primaryFilterName = primaryFilterName.call(this);
|
||||
}
|
||||
|
||||
let dataBoolFilterList = options.boolFilterList;
|
||||
|
||||
if (typeof options.boolFilterList === 'string') {
|
||||
dataBoolFilterList = options.boolFilterList.split(',');
|
||||
}
|
||||
|
||||
let boolFilterList = dataBoolFilterList || panelDefs.selectBoolFilterList;
|
||||
|
||||
if (typeof boolFilterList === 'function') {
|
||||
boolFilterList = boolFilterList.call(this);
|
||||
}
|
||||
|
||||
boolFilterList = Espo.Utils.clone(boolFilterList);
|
||||
|
||||
primaryFilterName = primaryFilterName || panelDefs.selectPrimaryFilterName || null;
|
||||
|
||||
const viewKey = options.viewKey || 'select';
|
||||
|
||||
const viewName = panelDefs.selectModalView ||
|
||||
this.metadata.get(['clientDefs', scope, 'modalViews', viewKey]) ||
|
||||
'views/modals/select-records';
|
||||
|
||||
Espo.Ui.notifyWait();
|
||||
|
||||
const handler = panelDefs.selectHandler || null;
|
||||
|
||||
new Promise(resolve => {
|
||||
if (!handler) {
|
||||
resolve({});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Espo.loader.requirePromise(handler)
|
||||
.then(Handler => new Handler(this.view.getHelper()))
|
||||
.then(/** import('contracts/relation').SelectRelatedHandler */handler => {
|
||||
handler.getFilters(model)
|
||||
.then(filters => resolve(filters));
|
||||
});
|
||||
}).then(filters => {
|
||||
advanced = {...advanced, ...(filters.advanced || {})};
|
||||
|
||||
if (boolFilterList || filters.bool) {
|
||||
boolFilterList = [
|
||||
...(boolFilterList || []),
|
||||
...(filters.bool || []),
|
||||
];
|
||||
}
|
||||
|
||||
if (filters.primary && !primaryFilterName) {
|
||||
primaryFilterName = filters.primary;
|
||||
}
|
||||
|
||||
const orderBy = filters.orderBy || panelDefs.selectOrderBy;
|
||||
const orderDirection = filters.orderBy ? filters.order : panelDefs.selectOrderDirection;
|
||||
|
||||
const createButton = options.hasCreate === true && options.onCreate !== undefined;
|
||||
|
||||
/** @type {import('views/modals/select-records').default} */
|
||||
let modalView;
|
||||
|
||||
this.view.createView('dialogSelectRelated', viewName, {
|
||||
scope: scope,
|
||||
multiple: true,
|
||||
filters: advanced,
|
||||
massRelateEnabled: massRelateEnabled,
|
||||
primaryFilterName: primaryFilterName,
|
||||
boolFilterList: boolFilterList,
|
||||
mandatorySelectAttributeList: panelDefs.selectMandatoryAttributeList,
|
||||
layoutName: panelDefs.selectLayout,
|
||||
orderBy: orderBy,
|
||||
orderDirection: orderDirection,
|
||||
createButton: createButton,
|
||||
onCreate: () => {
|
||||
modalView.close();
|
||||
|
||||
if (options.onCreate) {
|
||||
options.onCreate();
|
||||
}
|
||||
},
|
||||
}, view => {
|
||||
modalView = view;
|
||||
|
||||
view.render();
|
||||
|
||||
Espo.Ui.notify(false);
|
||||
|
||||
this.view.listenToOnce(view, 'select', (selectObj) => {
|
||||
const data = {};
|
||||
|
||||
if (Object.prototype.toString.call(selectObj) === '[object Array]') {
|
||||
const ids = [];
|
||||
|
||||
selectObj.forEach(model => ids.push(model.id));
|
||||
|
||||
data.ids = ids;
|
||||
} else if (selectObj.massRelate) {
|
||||
data.massRelate = true;
|
||||
data.where = selectObj.where;
|
||||
data.searchParams = selectObj.searchParams;
|
||||
} else {
|
||||
data.id = selectObj.id;
|
||||
}
|
||||
|
||||
const url = `${model.entityType}/${model.id}/${link}`;
|
||||
|
||||
Espo.Ajax.postRequest(url, data)
|
||||
.then(() => {
|
||||
Espo.Ui.success(this.view.translate('Linked'))
|
||||
|
||||
model.trigger(`update-related:${link}`);
|
||||
|
||||
model.trigger('after:relate');
|
||||
model.trigger(`after:relate:${link}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectRelatedHelper;
|
||||
Reference in New Issue
Block a user