').addClass('close-container').append($close));
$close.on('click', () => $el.alert('close'));
}
if (timeout) {
setTimeout(() => $el.alert('close'), timeout);
}
$el.appendTo('body');
},
/**
* Show a warning message.
*
* @param {string} message A message.
* @param {Espo.Ui~NotifyOptions} [options] Options.
*/
warning: function (message, options) {
Espo.Ui.notify(message, 'warning', 2000, options);
},
/**
* Show a success message.
*
* @param {string} message A message.
* @param {Espo.Ui~NotifyOptions} [options] Options.
*/
success: function (message, options) {
Espo.Ui.notify(message, 'success', 2000, options);
},
/**
* Show an error message.
*
* @param {string} message A message.
* @param {Espo.Ui~NotifyOptions|true} [options] Options. If true, then only closeButton option will be applied.
*/
error: function (message, options) {
options = typeof options === 'boolean' ? {
closeButton: options
} : {
...options
};
const timeout = options.closeButton ? 0 : 4000;
Espo.Ui.notify(message, 'danger', timeout, options);
},
/**
* Show an info message.
*
* @param {string} message A message.
* @param {Espo.Ui~NotifyOptions} [options] Options.
*/
info: function (message, options) {
Espo.Ui.notify(message, 'info', 2000, options);
}
};
let notifySuppressed = false;
/**
* @deprecated Use `Espo.Ui`.
*/
Espo.ui = Espo.Ui;
var _default = _exports.default = Espo.Ui;
});
define("theme-manager", ["exports"], function (_exports) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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
.
*
* 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 theme-manager */
/**
* A theme manager.
*/
class ThemeManager {
/**
* @param {module:models/settings} config A config.
* @param {module:models/preferences} preferences Preferences.
* @param {module:metadata} metadata Metadata.
* @param {string|null} [name] A name. If not set, then will be obtained from config and preferences.
*/
constructor(config, preferences, metadata, name) {
/**
* @private
* @type {module:models/settings}
*/
this.config = config;
/**
* @private
* @type {module:models/preferences}
*/
this.preferences = preferences;
/**
* @private
* @type {module:metadata}
*/
this.metadata = metadata;
/**
* @private
* @type {boolean}
*/
this.useConfig = !name;
/**
* @private
* @type {string|null}
*/
this.name = name || null;
}
/**
* @private
*/
defaultParams = {
screenWidthXs: 768,
dashboardCellHeight: 40,
dashboardCellMargin: 16
};
/**
* Get a theme name for the current user.
*
* @returns {string}
*/
getName() {
if (this.name) {
return this.name;
}
if (!this.config.get('userThemesDisabled')) {
const name = this.preferences.get('theme');
if (name && name !== '') {
return name;
}
}
return this.config.get('theme');
}
/**
* Get a theme name currently applied to the DOM.
*
* @returns {string|null} Null if not applied.
*/
getAppliedName() {
const name = window.getComputedStyle(document.body).getPropertyValue('--theme-name');
if (!name) {
return null;
}
return name.trim();
}
/**
* Whether a current theme is applied to the DOM.
*
* @returns {boolean}
*/
isApplied() {
const appliedName = this.getAppliedName();
if (!appliedName) {
return true;
}
return this.getName() === appliedName;
}
/**
* Get a stylesheet path for a current theme.
*
* @returns {string}
*/
getStylesheet() {
let link = this.getParam('stylesheet') || 'client/css/espo/espo.css';
if (this.config.get('cacheTimestamp')) {
link += '?r=' + this.config.get('cacheTimestamp').toString();
}
return link;
}
/**
* Get an iframe stylesheet path for a current theme.
*
* @returns {string}
*/
getIframeStylesheet() {
let link = this.getParam('stylesheetIframe') || 'client/css/espo/espo-iframe.css';
if (this.config.get('cacheTimestamp')) {
link += '?r=' + this.config.get('cacheTimestamp').toString();
}
return link;
}
/**
* Get an iframe-fallback stylesheet path for a current theme.
*
* @returns {string}
*/
getIframeFallbackStylesheet() {
let link = this.getParam('stylesheetIframeFallback') || 'client/css/espo/espo-iframe.css';
if (this.config.get('cacheTimestamp')) {
link += '?r=' + this.config.get('cacheTimestamp').toString();
}
return link;
}
/**
* Get a theme parameter.
*
* @param {string} name A parameter name.
* @returns {*} Null if not set.
*/
getParam(name) {
if (name !== 'params' && name !== 'mappedParams') {
const varValue = this.getVarParam(name);
if (varValue !== null) {
return varValue;
}
const mappedValue = this.getMappedParam(name);
if (mappedValue !== null) {
return mappedValue;
}
}
let value = this.metadata.get(['themes', this.getName(), name]);
if (value !== null) {
return value;
}
value = this.metadata.get(['themes', this.getParentName(), name]);
if (value !== null) {
return value;
}
return this.defaultParams[name] || null;
}
/**
* @private
* @param {string} name
* @returns {*}
*/
getVarParam(name) {
const params = this.getParam('params') || {};
if (!(name in params)) {
return null;
}
let values = null;
if (this.useConfig && !this.config.get('userThemesDisabled') && this.preferences.get('theme')) {
values = this.preferences.get('themeParams');
}
if (!values && this.useConfig) {
values = this.config.get('themeParams');
}
if (values && name in values) {
return values[name];
}
if ('default' in params[name]) {
return params[name].default;
}
return null;
}
/**
* @private
* @param {string} name
* @returns {*}
*/
getMappedParam(name) {
const mappedParams = this.getParam('mappedParams') || {};
if (!(name in mappedParams)) {
return null;
}
const mapped = mappedParams[name].param;
const valueMap = mappedParams[name].valueMap;
if (mapped && valueMap) {
const key = this.getParam(mapped);
return valueMap[key];
}
return null;
}
/**
* @private
* @returns {string}
*/
getParentName() {
return this.metadata.get(['themes', this.getName(), 'parent']) || 'Espo';
}
/**
* Whether a current theme is different from a system default theme.
*
* @returns {boolean}
*/
isUserTheme() {
if (this.config.get('userThemesDisabled')) {
return false;
}
const name = this.preferences.get('theme');
if (!name || name === '') {
return false;
}
return name !== this.config.get('theme');
}
/**
* Get a font-size factor. To adjust px sizes based on font-size.
*
* @return {number}
* @since 9.0.0
* @internal Experimental.
*/
getFontSizeFactor() {
const paramFontSize = this.getParam('fontSize') || 14;
const fontSize = parseInt(getComputedStyle(document.body).fontSize);
return Math.round(fontSize / paramFontSize * 10000) / 10000;
}
}
var _default = _exports.default = ThemeManager;
});
define("session-storage", ["exports"], function (_exports) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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
.
*
* 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 session-storage */
/**
* A session storage. Cleared when a page session ends.
*/
class SessionStorage {
/** @private */
storageObject = sessionStorage;
/**
* Get a value.
*
* @param {string} name A name.
* @returns {*} Null if not set.
*/
get(name) {
let stored;
try {
stored = this.storageObject.getItem(name);
} catch (error) {
console.error(error);
return null;
}
if (stored) {
let result = stored;
if (stored.length > 9 && stored.substring(0, 9) === '__JSON__:') {
const jsonString = stored.slice(9);
try {
result = JSON.parse(jsonString);
} catch (error) {
result = stored;
}
}
return result;
}
return null;
}
/**
* Set (store) a value.
*
* @param {string} name A name.
* @param {*} value A value.
*/
set(name, value) {
if (value === null) {
this.clear(name);
return;
}
if (value instanceof Object || Array.isArray(value) || value === true || value === false || typeof value === 'number') {
value = '__JSON__:' + JSON.stringify(value);
}
try {
this.storageObject.setItem(name, value);
} catch (error) {
console.error(error);
}
}
/**
* Has a value.
*
* @param {string} name A name.
* @returns {boolean}
*/
has(name) {
return this.storageObject.getItem(name) !== null;
}
/**
* Clear a value.
*
* @param {string} name A name.
*/
clear(name) {
for (const i in this.storageObject) {
if (i === name) {
delete this.storageObject[i];
}
}
}
}
var _default = _exports.default = SessionStorage;
});
define("page-title", ["exports", "jquery"], function (_exports, _jquery) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
_jquery = _interopRequireDefault(_jquery);
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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
.
*
* 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 page-title */
/**
* A page-title util.
*/
class PageTitle {
/**
* @class
* @param {module:models/settings} config A config.
*/
constructor(config) {
/**
* @private
* @type {boolean}
*/
this.displayNotificationNumber = config.get('newNotificationCountInTitle') || false;
/**
* @private
* @type {string}
*/
this.title = (0, _jquery.default)('head title').text() || '';
/**
* @private
* @type {number}
*/
this.notificationNumber = 0;
}
/**
* Set a title.
*
* @param {string} title A title.
*/
setTitle(title) {
this.title = title;
this.update();
}
/**
* Set a notification number.
*
* @param {number} notificationNumber A number.
*/
setNotificationNumber(notificationNumber) {
this.notificationNumber = notificationNumber;
if (this.displayNotificationNumber) {
this.update();
}
}
/**
* Update a page title.
*/
update() {
let value = '';
if (this.displayNotificationNumber && this.notificationNumber) {
value = '(' + this.notificationNumber.toString() + ')';
if (this.title) {
value += ' ';
}
}
value += this.title;
(0, _jquery.default)('head title').text(value);
}
}
var _default = _exports.default = PageTitle;
});
define("number-util", ["exports"], function (_exports) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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
.
*
* 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 num-util */
/**
* A number util.
*/
class NumberUtil {
/**
* @param {module:models/settings} config A config.
* @param {module:models/preferences} preferences Preferences.
*/
constructor(config, preferences) {
/**
* @private
* @type {module:models/settings}
*/
this.config = config;
/**
* @private
* @type {module:models/preferences}
*/
this.preferences = preferences;
/**
* A thousand separator.
*
* @private
* @type {string|null}
*/
this.thousandSeparator = null;
/**
* A decimal mark.
*
* @private
* @type {string|null}
*/
this.decimalMark = null;
this.config.on('change', () => {
this.thousandSeparator = null;
this.decimalMark = null;
});
this.preferences.on('change', () => {
this.thousandSeparator = null;
this.decimalMark = null;
});
/**
* A max decimal places.
*
* @private
* @type {number}
*/
this.maxDecimalPlaces = 10;
}
/**
* Format an integer number.
*
* @param {number} value A value.
* @returns {string}
*/
formatInt(value) {
if (value === null || value === undefined) {
return '';
}
let stringValue = value.toString();
stringValue = stringValue.replace(/\B(?=(\d{3})+(?!\d))/g, this.getThousandSeparator());
return stringValue;
}
// noinspection JSUnusedGlobalSymbols
/**
* Format a float number.
*
* @param {number} value A value.
* @param {number} [decimalPlaces] Decimal places.
* @returns {string}
*/
formatFloat(value, decimalPlaces) {
if (value === null || value === undefined) {
return '';
}
if (decimalPlaces === 0) {
value = Math.round(value);
} else if (decimalPlaces) {
value = Math.round(value * Math.pow(10, decimalPlaces)) / Math.pow(10, decimalPlaces);
} else {
value = Math.round(value * Math.pow(10, this.maxDecimalPlaces)) / Math.pow(10, this.maxDecimalPlaces);
}
const parts = value.toString().split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, this.getThousandSeparator());
if (decimalPlaces === 0) {
return parts[0];
}
if (decimalPlaces) {
let decimalPartLength = 0;
if (parts.length > 1) {
decimalPartLength = parts[1].length;
} else {
parts[1] = '';
}
if (decimalPlaces && decimalPartLength < decimalPlaces) {
const limit = decimalPlaces - decimalPartLength;
for (let i = 0; i < limit; i++) {
parts[1] += '0';
}
}
}
return parts.join(this.getDecimalMark());
}
/**
* @private
* @returns {string}
*/
getThousandSeparator() {
if (this.thousandSeparator !== null) {
return this.thousandSeparator;
}
let thousandSeparator = '.';
if (this.preferences.has('thousandSeparator')) {
thousandSeparator = this.preferences.get('thousandSeparator');
} else if (this.config.has('thousandSeparator')) {
thousandSeparator = this.config.get('thousandSeparator');
}
/**
* A thousand separator.
*
* @private
* @type {string|null}
*/
this.thousandSeparator = thousandSeparator;
return thousandSeparator;
}
/**
* @private
* @returns {string}
*/
getDecimalMark() {
if (this.decimalMark !== null) {
return this.decimalMark;
}
let decimalMark = '.';
if (this.preferences.has('decimalMark')) {
decimalMark = this.preferences.get('decimalMark');
} else {
if (this.config.has('decimalMark')) {
decimalMark = this.config.get('decimalMark');
}
}
/**
* A decimal mark.
*
* @private
* @type {string|null}
*/
this.decimalMark = decimalMark;
return decimalMark;
}
}
var _default = _exports.default = NumberUtil;
});
define("email-helper", ["exports", "di", "language", "models/user", "date-time", "acl-manager"], function (_exports, _di, _language, _user, _dateTime, _aclManager) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
_language = _interopRequireDefault(_language);
_user = _interopRequireDefault(_user);
_dateTime = _interopRequireDefault(_dateTime);
_aclManager = _interopRequireDefault(_aclManager);
let _init_language, _init_extra_language, _init_user, _init_extra_user, _init_dateTime, _init_extra_dateTime, _init_acl, _init_extra_acl;
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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
.
*
* 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 email-helper */
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 != (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
/**
* An email helper.
*/
class EmailHelper {
static #_ = [_init_language, _init_extra_language, _init_user, _init_extra_user, _init_dateTime, _init_extra_dateTime, _init_acl, _init_extra_acl] = _applyDecs(this, [], [[(0, _di.inject)(_language.default), 0, "language"], [(0, _di.inject)(_user.default), 0, "user"], [(0, _di.inject)(_dateTime.default), 0, "dateTime"], [(0, _di.inject)(_aclManager.default), 0, "acl"]]).e;
/**
* @private
* @type {Language}
*/
language = _init_language(this);
/**
* @private
* @type {User}
*/
user = (_init_extra_language(this), _init_user(this));
/**
* @private
* @type {DateTime}
*/
dateTime = (_init_extra_user(this), _init_dateTime(this));
/**
* @private
* @type {AclManager}
*/
acl = (_init_extra_dateTime(this), _init_acl(this));
constructor() {
_init_extra_acl(this);
/** @private */
this.erasedPlaceholder = 'ERASED:';
}
/**
* Get reply email attributes.
*
* @param {module:model} model An email model.
* @param {Object|null} [data=null] Action data. Unused.
* @param {boolean} [cc=false] To include CC (reply-all).
* @returns {Object.
}
*/
getReplyAttributes(model, data, cc) {
const attributes = {
status: 'Draft',
isHtml: model.attributes.isHtml
};
const subject = model.attributes.name || '';
attributes['name'] = subject.toUpperCase().indexOf('RE:') !== 0 ? 'Re: ' + subject : subject;
let to = '';
let isReplyOnSent = false;
const nameHash = model.attributes.nameHash || {};
const replyToAddressString = model.attributes.replyTo || null;
const replyToString = model.attributes.replyToString || null;
const userEmailAddressList = this.user.attributes.emailAddressList || [];
const idHash = model.attributes.idHash || {};
const typeHash = model.attributes.typeHash || {};
if (replyToAddressString) {
const replyToAddressList = replyToAddressString.split(';');
to = replyToAddressList.join(';');
} else if (replyToString) {
const a = [];
replyToString.split(';').forEach(item => {
const part = item.trim();
const address = this.parseAddressFromStringAddress(item);
if (address) {
a.push(address);
const name = this.parseNameFromStringAddress(part);
if (name && name !== address) {
nameHash[address] = name;
}
}
});
to = a.join(';');
}
if ((!to || !to.includes('@')) && model.attributes.from) {
if (!userEmailAddressList.includes(model.attributes.from)) {
to = model.attributes.from;
if (!nameHash[to]) {
const fromString = model.attributes.fromString || model.attributes.fromName;
if (fromString) {
const name = this.parseNameFromStringAddress(fromString);
if (name !== to) {
nameHash[to] = name;
}
}
}
} else {
isReplyOnSent = true;
}
}
attributes.to = to;
if (cc) {
attributes.cc = model.attributes.cc || '';
/** @type {string[]} */
const excludeFromReplyEmailAddressList = this.user.get('excludeFromReplyEmailAddressList') || [];
(model.get('to') || '').split(';').forEach(item => {
item = item.trim();
if (item === this.user.get('emailAddress')) {
return;
}
if (excludeFromReplyEmailAddressList.includes(item)) {
return;
}
if (isReplyOnSent) {
if (attributes.to) {
attributes.to += ';';
}
attributes.to += item;
return;
}
if (attributes.cc) {
attributes.cc += ';';
}
attributes.cc += item;
});
attributes.cc = attributes.cc.replace(/^(; )/, "");
}
if (attributes.to) {
let toList = attributes.to.split(';');
toList = toList.filter(item => {
if (item.indexOf(this.erasedPlaceholder) === 0) {
return false;
}
return true;
});
attributes.to = toList.join(';');
}
/** @type {string[]} */
const personalAddresses = this.user.get('userEmailAddressList') || [];
const lcPersonalAddresses = personalAddresses.map(it => it.toLowerCase());
if (attributes.cc) {
const ccList = attributes.cc.split(';').filter(item => {
if (lcPersonalAddresses.includes(item.toLowerCase())) {
return false;
}
if (item.indexOf(this.erasedPlaceholder) === 0) {
return false;
}
return true;
});
attributes.cc = ccList.join(';');
}
if (model.get('parentId')) {
attributes['parentId'] = model.get('parentId');
attributes['parentName'] = model.get('parentName');
attributes['parentType'] = model.get('parentType');
}
if (model.get('teamsIds') && model.get('teamsIds').length) {
attributes.teamsIds = Espo.Utils.clone(model.get('teamsIds'));
attributes.teamsNames = Espo.Utils.clone(model.get('teamsNames') || {});
const defaultTeamId = this.user.get('defaultTeamId');
if (defaultTeamId && !~attributes.teamsIds.indexOf(defaultTeamId)) {
attributes.teamsIds.push(this.user.get('defaultTeamId'));
attributes.teamsNames[this.user.get('defaultTeamId')] = this.user.get('defaultTeamName');
}
attributes.teamsIds = attributes.teamsIds.filter(teamId => this.acl.checkTeamAssignmentPermission(teamId));
}
attributes.nameHash = nameHash;
attributes.typeHash = typeHash;
attributes.idHash = idHash;
attributes.repliedId = model.id;
attributes.inReplyTo = model.get('messageId');
/** @type {string[]} */
const lcToAddresses = (model.attributes.to || '').split(';').map(it => it.toLowerCase());
for (const address of personalAddresses) {
if (lcToAddresses.includes(address.toLowerCase())) {
attributes.from = address;
break;
}
}
this.addReplyBodyAttributes(model, attributes);
return attributes;
}
/**
* Get forward email attributes.
*
* @param {module:model} model An email model.
* @returns {Object}
*/
getForwardAttributes(model) {
const attributes = {
status: 'Draft',
isHtml: model.get('isHtml')
};
const subject = model.get('name');
if (~!subject.toUpperCase().indexOf('FWD:') && ~!subject.toUpperCase().indexOf('FW:')) {
attributes['name'] = 'Fwd: ' + subject;
} else {
attributes['name'] = subject;
}
if (model.get('parentId')) {
attributes['parentId'] = model.get('parentId');
attributes['parentName'] = model.get('parentName');
attributes['parentType'] = model.get('parentType');
}
this.addForwardBodyAttributes(model, attributes);
return attributes;
}
/**
* Add body attributes for a forward email.
*
* @param {module:model} model An email model.
* @param {Object} attributes
*/
addForwardBodyAttributes(model, attributes) {
let prepending = '';
if (model.get('isHtml')) {
prepending = '
' + '------' + this.language.translate('Forwarded message', 'labels', 'Email') + '------';
} else {
prepending = '\n\n' + '------' + this.language.translate('Forwarded message', 'labels', 'Email') + '------';
}
const list = [];
if (model.get('from')) {
const from = model.get('from');
let line = this.language.translate('from', 'fields', 'Email') + ': ';
const nameHash = model.get('nameHash') || {};
if (from in nameHash) {
line += nameHash[from] + ' ';
}
if (model.get('isHtml')) {
line += '<' + from + '>';
} else {
line += '<' + from + '>';
}
list.push(line);
}
if (model.get('dateSent')) {
let line = this.language.translate('dateSent', 'fields', 'Email') + ': ';
line += this.dateTime.toDisplay(model.get('dateSent'));
list.push(line);
}
if (model.get('name')) {
let line = this.language.translate('subject', 'fields', 'Email') + ': ';
line += model.get('name');
list.push(line);
}
if (model.get('to')) {
let line = this.language.translate('to', 'fields', 'Email') + ': ';
const partList = [];
model.get('to').split(';').forEach(to => {
const nameHash = model.get('nameHash') || {};
let line = '';
if (to in nameHash) {
line += nameHash[to] + ' ';
}
if (model.get('isHtml')) {
line += '<' + to + '>';
} else {
line += '<' + to + '>';
}
partList.push(line);
});
line += partList.join(';');
list.push(line);
}
list.forEach(line => {
if (model.get('isHtml')) {
prepending += '
' + line;
} else {
prepending += '\n' + line;
}
});
if (model.get('isHtml')) {
const body = model.get('body');
attributes['body'] = prepending + '
' + body;
} else {
const bodyPlain = model.get('body') || model.get('bodyPlain') || '';
attributes['bodyPlain'] = attributes['body'] = prepending + '\n\n' + bodyPlain;
}
}
/**
* Parse a name from a string address.
*
* @param {string} value A string address. E.g. `Test Name `.
* @returns {string|null}
*/
parseNameFromStringAddress(value) {
if (!value.includes('<')) {
return null;
}
let name = value.replace(/<(.*)>/, '').trim();
if (name.charAt(0) === '"' && name.charAt(name.length - 1) === '"') {
name = name.slice(1, name.length - 2);
}
return name;
}
/**
* Parse an address from a string address.
*
* @param {string} value A string address. E.g. `Test Name `.
* @returns {string|null}
*/
parseAddressFromStringAddress(value) {
const r = value.match(/<(.*)>/);
let address;
if (r && r.length > 1) {
address = r[1];
} else {
address = value.trim();
}
return address;
}
/**
* Add body attributes for a reply email.
*
* @param {module:model} model An email model.
* @param {Object.} attributes
*/
addReplyBodyAttributes(model, attributes) {
const format = this.dateTime.getReadableShortDateTimeFormat();
const dateSent = model.get('dateSent');
let dateSentString = null;
if (dateSent) {
const dateSentMoment = this.dateTime.toMoment(dateSent);
dateSentString = dateSentMoment.format(format);
if (dateSentMoment.year() !== this.dateTime.getNowMoment().year()) {
dateSentString += ', ' + dateSentMoment.year();
}
}
let replyHeadString = dateSentString || this.language.translate('Original message', 'labels', 'Email');
let fromName = model.get('fromName');
if (!fromName && model.get('from')) {
fromName = (model.get('nameHash') || {})[model.get('from')];
if (fromName) {
replyHeadString += ', ' + fromName;
}
}
replyHeadString += ':';
if (model.get('isHtml')) {
const body = model.get('body');
attributes['body'] = `
` + `${replyHeadString}
${body}
`;
return;
}
let bodyPlain = model.get('body') || model.get('bodyPlain') || '';
let b = '\n\n';
b += replyHeadString + '\n';
bodyPlain.split('\n').forEach(line => {
b += '> ' + line + '\n';
});
bodyPlain = b;
attributes['body'] = bodyPlain;
attributes['bodyPlain'] = bodyPlain;
}
}
var _default = _exports.default = EmailHelper;
});
define("collection", ["exports", "model", "bullbone", "underscore"], function (_exports, _model, _bullbone, _underscore) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
_model = _interopRequireDefault(_model);
_underscore = _interopRequireDefault(_underscore);
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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 .
*
* 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 collection */
/**
* On sync with backend.
*
* @event Collection#sync
* @param {Collection} collection A collection.
* @param {Object} response Response from backend.
* @param {Object} o Options.
*/
/**
* Any number of models have been added, removed or changed.
*
* @event Collection#update
* @param {Collection} collection A collection.
* @param {Object} o Options.
*/
/**
* On reset.
*
* @event Collection#reset
* @param {Collection} collection A collection.
* @param {Object} o Options.
*/
/**
* On model sync.
*
* @event Collection#model-sync
* @param {Model} model A model.
* @param {Record & {action?: 'fetch'|'save'|'destroy'}} o Options.
* @since 9.1.0
*/
/**
* @typedef {Object} module:collection~Data
* @property {string|null} [primaryFilter]
* @property {string[]} [boolFilterList]
* @property {string} [textFilter]
* @property {string} [select]
* @property {string} [q]
*/
/**
* A collection.
*
* @mixes Bull.Events
* @copyright Credits to Backbone.js.
*/
class Collection {
/**
* An entity type.
*
* @type {string|null}
*/
entityType = null;
/**
* A total number of records.
*
* @type {number}
*/
total = 0;
/**
* A current offset (for pagination).
*
* @type {number}
*/
offset = 0;
/**
* A max size (for pagination).
*
* @type {number}
*/
maxSize = 20;
/**
* An order.
*
* @type {boolean|'asc'|'desc'|null}
*/
order = null;
/**
* An order-by field.
*
* @type {string|null}
*/
orderBy = null;
/**
* A where clause.
*
* @type {module:search-manager~whereItem[]|null}
*/
where = null;
/**
* @deprecated
* @type {module:search-manager~whereItem[]|null}
*/
whereAdditional = null;
/**
* A length correction.
*
* @type {number}
*/
lengthCorrection = 0;
/**
* A max max-size.
*
* @type {number}
*/
maxMaxSize = 0;
/**
* A where function.
*
* @type {function(): module:search-manager~whereItem[]}
*/
whereFunction;
/**
* A last sync request promise.
*
* @type {module:ajax.AjaxPromise|null}
*/
lastSyncPromise = null;
/**
* A parent model. To be used for own purposes. E.g. to have access to a parent from related models.
*
* @type {import('model').default}
*/
parentModel;
/**
* @param {Model[]|Record[]|null} [models] Models.
* @param {{
* entityType?: string,
* model?: Model.prototype,
* defs?: module:model~defs,
* order?: 'asc'|'desc'|boolean|null,
* orderBy?: string|null,
* urlRoot?: string,
* url?: string,
* maxSize?: number,
* }} [options] Options.
*/
constructor(models, options) {
options = {
...options
};
if (options.model) {
this.model = options.model;
}
if (options.maxSize !== undefined) {
this.maxSize = options.maxSize;
}
this._reset();
if (options.entityType) {
this.entityType = options.entityType;
/** @deprecated */
this.name = this.entityType;
}
/**
* A root URL.
*
* @public
* @type {string|null}
*/
this.urlRoot = options.urlRoot || this.urlRoot || this.entityType;
/**
* An URL.
*
* @type {string|null}
*/
this.url = options.url || this.url || this.urlRoot;
this.orderBy = options.orderBy || this.orderBy;
this.order = options.order || this.order;
this.defaultOrder = this.order;
this.defaultOrderBy = this.orderBy;
/** @type {module:model~defs} */
this.defs = options.defs || {};
/**
* @type {module:collection~Data | Record}
*/
this.data = {};
/**
* @private
* @type {Model#}
*/
this.model = options.model || _model.default;
if (models) {
this.reset(models, {
silent: true,
...options
});
}
}
/**
* Add models or a model.
*
* @param {Model[]|Model|Record[]|Record} models Models ar a model.
* @param {{
* merge?: boolean,
* at?: number,
* silent?: boolean,
* }} [options] Options. `at` – position; `merge` – merge existing models, otherwise, they are ignored.
* @return {this}
* @fires Collection#update
*/
add(models, options) {
this.set(models, {
merge: false,
...options,
...addOptions
});
return this;
}
/**
* Remove models or a model.
*
* @param {Model[]|Model|string} models Models, a model or a model ID.
* @param {{
* silent?: boolean,
* } & Object.} [options] Options.
* @return {this}
* @fires Collection#update
*/
remove(models, options) {
options = {
...options
};
const singular = !_underscore.default.isArray(models);
models = singular ? [models] : models.slice();
const removed = this._removeModels(models, options);
if (!options.silent && removed.length) {
options.changes = {
added: [],
merged: [],
removed: removed
};
this.trigger('update', this, options);
}
return this;
}
/**
* @protected
* @param {Model[]|Model|Record[]} models Models ar a model.
* @param {{
* silent?: boolean,
* at?: number,
* prepare?: boolean,
* add?: boolean,
* merge?: boolean,
* remove?: boolean,
* index?: number,
* } & Object.} [options]
* @return {Model[]}
*/
set(models, options) {
if (models == null) {
return [];
}
options = {
...setOptions,
...options
};
if (options.prepare && !this._isModel(models)) {
models = this.prepareAttributes(models, options) || [];
}
const singular = !_underscore.default.isArray(models);
models = singular ? [models] : models.slice();
let at = options.at;
if (at != null) {
at = +at;
}
if (at > this.length) {
at = this.length;
}
if (at < 0) {
at += this.length + 1;
}
const set = [];
const toAdd = [];
const toMerge = [];
const toRemove = [];
const modelMap = {};
const add = options.add;
const merge = options.merge;
const remove = options.remove;
let model, i;
for (i = 0; i < models.length; i++) {
model = models[i];
const existing = this._get(model);
if (existing) {
if (merge && model !== existing) {
let attributes = this._isModel(model) ? model.attributes : model;
if (options.prepare) {
attributes = existing.prepareAttributes(attributes, options);
}
existing.set(attributes, options);
toMerge.push(existing);
}
if (!modelMap[existing.cid]) {
modelMap[existing.cid] = true;
set.push(existing);
}
models[i] = existing;
} else if (add) {
model = models[i] = this._prepareModel(model);
if (model) {
toAdd.push(model);
this._addReference(model, options);
modelMap[model.cid] = true;
set.push(model);
}
}
}
// Remove stale models.
if (remove) {
for (i = 0; i < this.length; i++) {
model = this.models[i];
if (!modelMap[model.cid]) {
toRemove.push(model);
}
}
if (toRemove.length) {
this._removeModels(toRemove, options);
}
}
let orderChanged = false;
const replace = add && remove;
if (set.length && replace) {
orderChanged = this.length !== set.length || _underscore.default.some(this.models, (m, index) => {
return m !== set[index];
});
this.models.length = 0;
splice(this.models, set, 0);
this.length = this.models.length;
} else if (toAdd.length) {
splice(this.models, toAdd, at == null ? this.length : at);
this.length = this.models.length;
}
if (!options.silent) {
for (i = 0; i < toAdd.length; i++) {
if (at != null) {
options.index = at + i;
}
model = toAdd[i];
model.trigger('add', model, this, options);
}
if (orderChanged) {
this.trigger('sort', this, options);
}
if (toAdd.length || toRemove.length || toMerge.length) {
options.changes = {
added: toAdd,
removed: toRemove,
merged: toMerge
};
this.trigger('update', this, options);
}
}
return models;
}
/**
* Reset.
*
* @param {Model[]|null} [models] Models to replace the collection with.
* @param {{
* silent?: boolean,
* } & Object.} [options]
* @return {this}
* @fires Collection#reset
*/
reset(models, options) {
this.lengthCorrection = 0;
options = options ? _underscore.default.clone(options) : {};
for (let i = 0; i < this.models.length; i++) {
this._removeReference(this.models[i], options);
}
options.previousModels = this.models;
this._reset();
if (models) {
this.add(models, {
silent: true,
...options
});
}
if (!options.silent) {
this.trigger('reset', this, options);
}
return this;
}
/**
* Add a model at the end.
*
* @param {Model} model A model.
* @param {{
* silent?: boolean,
* }} [options] Options
* @return {this}
*/
push(model, options) {
this.add(model, {
at: this.length,
...options
});
return this;
}
/**
* Remove and return the last model.
*
* @param {{
* silent?: boolean,
* }} [options] Options
* @return {Model|null}
*/
pop(options) {
const model = this.at(this.length - 1);
if (!model) {
return null;
}
this.remove(model, options);
return model;
}
/**
* Add a model to the beginning.
*
* @param {Model} model A model.
* @param {{
* silent?: boolean,
* }} [options] Options
* @return {this}
*/
unshift(model, options) {
this.add(model, {
at: 0,
...options
});
return this;
}
/**
* Remove and return the first model.
*
* @param {{
* silent?: boolean,
* }} [options] Options
* @return {Model|null}
*/
shift(options) {
const model = this.at(0);
if (!model) {
return null;
}
this.remove(model, options);
return model;
}
/**
* Get a model by an ID.
*
* @todo Usage to _get.
* @param {string} id An ID.
* @return {Model|undefined}
*/
get(id) {
return this._get(id);
}
/**
* Whether a model in the collection.
*
* @todo Usage to _has.
* @param {string} id An ID.
* @return {boolean}
*/
has(id) {
return this._has(id);
}
/**
* Get a model by index.
*
* @param {number} index An index. Can be negative, then counted from the end.
* @return {Model|undefined}
*/
at(index) {
if (index < 0) {
index += this.length;
}
return this.models[index];
}
/**
* Iterates through a collection.
*
* @param {function(Model)} callback A function.
* @param {Object} [context] A context.
*/
forEach(callback, context) {
return this.models.forEach(callback, context);
}
/**
* Get an index of a model. Returns -1 if not found.
*
* @param {Model} model A model
* @return {number}
*/
indexOf(model) {
return this.models.indexOf(model);
}
/**
* @private
* @param {string|Object.|Model} obj
* @return {boolean}
*/
_has(obj) {
return !!this._get(obj);
}
/**
* @private
* @param {string|Object.|Model} obj
* @return {Model|undefined}
*/
_get(obj) {
if (obj == null) {
return void 0;
}
return this._byId[obj] || this._byId[this.modelId(obj.attributes || obj)] || obj.cid && this._byId[obj.cid];
}
/**
* @protected
* @param {Object.} attributes
* @return {*}
*/
modelId(attributes) {
return attributes['id'];
}
/** @private */
_reset() {
/**
* A number of records.
*/
this.length = 0;
/**
* Models.
*
* @type {Model[]}
*/
this.models = [];
/** @private */
this._byId = {};
}
/**
* @param {string} orderBy An order field.
* @param {bool|null|'desc'|'asc'} [order] True for desc.
* @returns {Promise}
*/
sort(orderBy, order) {
this.orderBy = orderBy;
if (order === true) {
order = 'desc';
} else if (order === false) {
order = 'asc';
}
this.order = order || 'asc';
return this.fetch();
}
/**
* Has previous page.
*
* @return {boolean}
*/
hasPreviousPage() {
return this.offset > 0;
}
/**
* Has next page.
*
* @return {boolean}
*/
hasNextPage() {
return this.total - this.offset > this.length || this.total === -1;
}
/**
* Next page.
*
* @returns {Promise}
*/
nextPage() {
return this.setOffset(this.offset + this.length);
}
/**
* Previous page.
*
* @returns {Promise}
*/
previousPage() {
return this.setOffset(Math.max(0, this.offset - this.maxSize));
}
/**
* First page.
*
* @returns {Promise}
*/
firstPage() {
return this.setOffset(0);
}
/**
* Last page.
*
* @returns {Promise}
*/
lastPage() {
let offset = this.total - this.total % this.maxSize;
if (offset === this.total) {
offset = this.total - this.maxSize;
}
return this.setOffset(offset);
}
/**
* Set an offset.
*
* @param {number} offset Offset.
* @returns {Promise}
*/
setOffset(offset) {
if (offset < 0) {
throw new RangeError('offset can not be less than 0');
}
if (offset > this.total && this.total !== -1 && this.total !== -2 && offset > 0) {
throw new RangeError('offset can not be larger than total count');
}
this.offset = offset;
return this.fetch({
maxSize: this.maxSize
});
}
/**
* Has more.
*
* @return {boolean}
*/
hasMore() {
return this.total > this.length + this.offset + this.lengthCorrection || this.total === -1;
}
/**
* Prepare attributes.
*
* @protected
* @param {Object.|Record[]} response A response from the backend.
* @param {Object.} options Options.
* @returns {Object.[]}
*/
prepareAttributes(response, options) {
this.total = response.total;
// noinspection JSUnusedGlobalSymbols
/**
* @deprecated As of v8.4. Use 'sync' event to obtain any additional data from a response.
*/
this.dataAdditional = response.additionalData || null;
return response.list;
}
/**
* Fetch from the backend.
*
* @param {{
* remove?: boolean,
* more?: boolean,
* offset?: number,
* maxSize?: number,
* orderBy?: string,
* order?: 'asc'|'desc',
* } & Object.} [options] Options.
* @returns {Promise}
* @fires Collection#sync Unless `{silent: true}`.
*/
fetch(options) {
options = {
...options
};
options.data = {
...options.data,
...this.data
};
this.offset = options.offset || this.offset;
this.orderBy = options.orderBy || this.orderBy;
this.order = options.order || this.order;
this.where = options.where || this.where;
const length = this.length + this.lengthCorrection;
if ('maxSize' in options) {
options.data.maxSize = options.maxSize;
} else {
options.data.maxSize = options.more ? this.maxSize : Math.max(length, this.maxSize);
if (this.maxMaxSize && options.data.maxSize > this.maxMaxSize) {
options.data.maxSize = this.maxMaxSize;
}
}
options.data.offset = options.more ? this.offset + length : this.offset;
options.data.orderBy = this.orderBy;
options.data.order = this.order;
options.data.whereGroup = this.getWhere();
if (options.data.select) {
options.data.attributeSelect = options.data.select;
delete options.data.select;
}
options = {
prepare: true,
...options
};
const success = options.success;
options.success = response => {
options.reset ? this.reset(response, options) : this.set(response, options);
if (success) {
success.call(options.context, this, response, options);
}
this.trigger('sync', this, response, options);
};
const error = options.error;
options.error = response => {
if (error) {
error.call(options.context, this, response, options);
}
this.trigger('error', this, response, options);
};
this.lastSyncPromise = _model.default.prototype.sync.call(this, 'read', this, options);
return this.lastSyncPromise;
}
/**
* Is being fetched.
*
* @return {boolean}
*/
isBeingFetched() {
return this.lastSyncPromise && this.lastSyncPromise.getReadyState() < 4;
}
/**
* Abort the last fetch.
*/
abortLastFetch() {
if (this.isBeingFetched()) {
this.lastSyncPromise.abort();
}
}
/**
* Get a where clause.
*
* @returns {module:search-manager~whereItem[]}
*/
getWhere() {
let where = (this.where ?? []).concat(this.whereAdditional || []);
if (this.whereFunction) {
where = where.concat(this.whereFunction() || []);
}
return where;
}
/**
* Get an entity type.
*
* @returns {string}
*/
getEntityType() {
return this.entityType || this.name;
}
/**
* Reset the order to default.
*/
resetOrderToDefault() {
this.orderBy = this.defaultOrderBy;
this.order = this.defaultOrder;
}
/**
* Set an order.
*
* @param {string|null} orderBy
* @param {boolean|'asc'|'desc'|null} [order]
* @param {boolean} [setDefault]
*/
setOrder(orderBy, order, setDefault) {
this.orderBy = orderBy;
this.order = order;
if (setDefault) {
this.defaultOrderBy = orderBy;
this.defaultOrder = order;
}
}
/**
* Clone.
*
* @param {{withModels?: boolean}} [options]
* @return {Collection}
*/
clone() {
let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
let models = this.models;
if (options.withModels) {
models = this.models.map(m => m.clone());
}
const collection = new this.constructor(models, {
model: this.model,
entityType: this.entityType,
defs: this.defs,
orderBy: this.orderBy,
order: this.order
});
collection.name = this.name;
collection.urlRoot = this.urlRoot;
collection.url = this.url;
collection.defaultOrder = this.defaultOrder;
collection.defaultOrderBy = this.defaultOrderBy;
collection.data = Espo.Utils.cloneDeep(this.data);
collection.where = Espo.Utils.cloneDeep(this.where);
collection.whereAdditional = Espo.Utils.cloneDeep(this.whereAdditional);
collection.total = this.total;
collection.offset = this.offset;
collection.maxSize = this.maxSize;
collection.maxMaxSize = this.maxMaxSize;
collection.whereFunction = this.whereFunction;
collection.parentModel = this.parentModel;
return collection;
}
/**
* Prepare an empty model instance.
*
* @return {Model}
*/
prepareModel() {
return this._prepareModel({});
}
// noinspection JSUnusedGlobalSymbols
/**
* Compose a URL for syncing. Called from Model.sync.
*
* @protected
* @return {string}
*/
composeSyncUrl() {
return this.url;
}
/** @private */
_isModel(object) {
return object instanceof _model.default;
}
/** @private */
_removeModels(models, options) {
const removed = [];
for (let i = 0; i < models.length; i++) {
const model = this.get(models[i]);
if (!model) {
continue;
}
const index = this.models.indexOf(model);
this.models.splice(index, 1);
this.length--;
delete this._byId[model.cid];
const id = this.modelId(model.attributes);
if (id != null) {
delete this._byId[id];
}
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
removed.push(model);
this._removeReference(model, options);
}
return removed;
}
/** @private */
_addReference(model) {
this._byId[model.cid] = model;
const id = this.modelId(model.attributes);
if (id != null) {
this._byId[id] = model;
}
model.on('all', this._onModelEvent, this);
}
/** @private */
_removeReference(model) {
delete this._byId[model.cid];
const id = this.modelId(model.attributes);
if (id != null) {
delete this._byId[id];
}
if (this === model.collection) {
delete model.collection;
}
model.off('all', this._onModelEvent, this);
}
/** @private */
_onModelEvent(event, model, collection, options) {
// @todo Revise. Never triggerred? Remove?
if (event === 'sync' && collection !== this) {
return;
}
if (!model) {
this.trigger.apply(this, arguments);
return;
}
if ((event === 'add' || event === 'remove') && collection !== this) {
return;
}
if (event === 'destroy') {
this.remove(model, options);
}
if (event === 'change') {
const prevId = this.modelId(model.previousAttributes());
const id = this.modelId(model.attributes);
if (prevId !== id) {
if (prevId != null) {
delete this._byId[prevId];
}
if (id != null) {
this._byId[id] = model;
}
}
}
this.trigger.apply(this, arguments);
}
// noinspection JSDeprecatedSymbols
/** @private*/
_prepareModel(attributes) {
if (this._isModel(attributes)) {
if (!attributes.collection) {
attributes.collection = this;
}
return attributes;
}
const ModelClass = this.model;
// noinspection JSValidateTypes
return new ModelClass(attributes, {
collection: this,
entityType: this.entityType || this.name,
defs: this.defs
});
}
}
Object.assign(Collection.prototype, _bullbone.Events);
Collection.extend = _bullbone.View.extend;
const setOptions = {
add: true,
remove: true,
merge: true
};
const addOptions = {
add: true,
remove: false
};
const splice = (array, insert, at) => {
at = Math.min(Math.max(at, 0), array.length);
const tail = Array(array.length - at);
const length = insert.length;
let i;
for (i = 0; i < tail.length; i++) {
tail[i] = array[i + at];
}
for (i = 0; i < length; i++) {
array[i + at] = insert[i];
}
for (i = 0; i < tail.length; i++) {
array[i + length + at] = tail[i];
}
};
var _default = _exports.default = Collection;
});
define("collection-factory", ["exports"], function (_exports) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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 .
*
* 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 collection-factory */
/**
* A collection factory.
*/
class CollectionFactory {
/**
* @param {module:model-factory} modelFactory
* @param {module:models/settings} config
* @param {module:metadata} metadata
*/
constructor(modelFactory, config, metadata) {
/** @private */
this.modelFactory = modelFactory;
/** @private */
this.metadata = metadata;
/** @private */
this.recordListMaxSizeLimit = config.get('recordListMaxSizeLimit') || 200;
}
/**
* Create a collection.
*
* @param {string} entityType An entity type.
* @param {Function} [callback] Deprecated.
* @param {Object} [context] Deprecated.
* @returns {Promise}
*/
create(entityType, callback, context) {
return new Promise(resolve => {
context = context || this;
this.modelFactory.getSeed(entityType, Model => {
const orderBy = this.modelFactory.metadata.get(['entityDefs', entityType, 'collection', 'orderBy']);
const order = this.modelFactory.metadata.get(['entityDefs', entityType, 'collection', 'order']);
const className = this.modelFactory.metadata.get(['clientDefs', entityType, 'collection']) || 'collection';
const defs = this.metadata.get(['entityDefs', entityType]) || {};
Espo.loader.require(className, Collection => {
const collection = new Collection(null, {
entityType: entityType,
orderBy: orderBy,
order: order,
defs: defs
});
collection.model = Model;
collection.entityType = entityType;
collection.maxMaxSize = this.recordListMaxSizeLimit;
if (callback) {
callback.call(context, collection);
}
resolve(collection);
});
});
});
}
}
var _default = _exports.default = CollectionFactory;
});
define("cache", ["exports"], function (_exports) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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 .
*
* 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 cache */
/**
* Cache for source and resource files.
*/
class Cache {
/**
* @param {Number} [cacheTimestamp] A cache timestamp.
*/
constructor(cacheTimestamp) {
this.basePrefix = this.prefix;
if (cacheTimestamp) {
this.prefix = this.basePrefix + '-' + cacheTimestamp;
}
if (!this.get('app', 'timestamp')) {
this.storeTimestamp();
}
}
/** @private */
prefix = 'cache';
/**
* Handle actuality. Clears cache if not actual.
*
* @param {Number} cacheTimestamp A cache timestamp.
*/
handleActuality(cacheTimestamp) {
const storedTimestamp = this.getCacheTimestamp();
if (storedTimestamp) {
if (storedTimestamp !== cacheTimestamp) {
this.clear();
this.set('app', 'cacheTimestamp', cacheTimestamp);
this.storeTimestamp();
}
return;
}
this.clear();
this.set('app', 'cacheTimestamp', cacheTimestamp);
this.storeTimestamp();
}
/**
* Get a cache timestamp.
*
* @returns {number}
*/
getCacheTimestamp() {
return parseInt(this.get('app', 'cacheTimestamp') || 0);
}
/**
* @todo Revise whether is needed.
*/
storeTimestamp() {
const frontendCacheTimestamp = Date.now();
this.set('app', 'timestamp', frontendCacheTimestamp);
}
/**
* @private
* @param {string} type
* @returns {string}
*/
composeFullPrefix(type) {
return this.prefix + '-' + type;
}
/**
* @private
* @param {string} type
* @param {string} name
* @returns {string}
*/
composeKey(type, name) {
return this.composeFullPrefix(type) + '-' + name;
}
/**
* @private
* @param {string} type
*/
checkType(type) {
if (typeof type === 'undefined' && toString.call(type) !== '[object String]') {
throw new TypeError("Bad type \"" + type + "\" passed to Cache().");
}
}
/**
* Get a stored value.
*
* @param {string} type A type/category.
* @param {string} name A name.
* @returns {string|null} Null if no stored value.
*/
get(type, name) {
this.checkType(type);
const key = this.composeKey(type, name);
let stored;
try {
stored = localStorage.getItem(key);
} catch (error) {
console.error(error);
return null;
}
if (stored) {
let result = stored;
if (stored.length > 9 && stored.substring(0, 9) === '__JSON__:') {
const jsonString = stored.slice(9);
try {
result = JSON.parse(jsonString);
} catch (error) {
result = stored;
}
}
return result;
}
return null;
}
/**
* Store a value.
*
* @param {string} type A type/category.
* @param {string} name A name.
* @param {any} value A value.
*/
set(type, name, value) {
this.checkType(type);
const key = this.composeKey(type, name);
if (value instanceof Object || Array.isArray(value)) {
value = '__JSON__:' + JSON.stringify(value);
}
try {
localStorage.setItem(key, value);
} catch (error) {
console.log('Local storage limit exceeded.');
}
}
/**
* Clear a stored value.
*
* @param {string} [type] A type/category.
* @param {string} [name] A name.
*/
clear(type, name) {
let reText;
if (typeof type !== 'undefined') {
if (typeof name === 'undefined') {
reText = '^' + this.composeFullPrefix(type);
} else {
reText = '^' + this.composeKey(type, name);
}
} else {
reText = '^' + this.basePrefix + '-';
}
const re = new RegExp(reText);
for (const i in localStorage) {
if (re.test(i)) {
delete localStorage[i];
}
}
}
}
var _default = _exports.default = Cache;
});
define("broadcast-channel", ["exports"], function (_exports) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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 .
*
* 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 broadcast-channel */
class BroadcastChannel {
constructor() {
this.object = null;
if (window.BroadcastChannel) {
this.object = new window.BroadcastChannel('app');
}
}
/**
* Post a message.
*
* @param {string} message A message.
*/
postMessage(message) {
if (!this.object) {
return;
}
this.object.postMessage(message);
}
/**
* @callback module:broadcast-channel~callback
*
* @param {MessageEvent} event An event. A message can be obtained from the `data` property.
*/
/**
* Subscribe to a message.
*
* @param {module:broadcast-channel~callback} callback A callback.
*/
subscribe(callback) {
if (!this.object) {
return;
}
this.object.addEventListener('message', callback);
}
}
var _default = _exports.default = BroadcastChannel;
});
define("app-params", ["exports"], function (_exports) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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 .
*
* 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.
************************************************************************/
/**
* Application parameters.
*
* @since 9.0.0
*/
class AppParams {
/**
* @param {Record} params
*/
constructor() {
let params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
/** @private */
this.params = params;
}
/**
* Get a parameter.
*
* @param {string} name A parameter.
* @return {*}
*/
get(name) {
return this.params[name];
}
/**
* Set all parameters.
*
* @internal
* @param {Record} params
*/
setAll(params) {
this.params = params;
}
/**
* Reload params from the backend.
*/
async load() {
/** @type {module:app~UserData} */
const data = await Espo.Ajax.getRequest('App/user');
this.params = data.appParams;
}
}
_exports.default = AppParams;
});
define("ajax", ["exports", "jquery", "utils"], function (_exports, _jquery, _utils) {
"use strict";
Object.defineProperty(_exports, "__esModule", {
value: true
});
_exports.default = void 0;
_jquery = _interopRequireDefault(_jquery);
_utils = _interopRequireDefault(_utils);
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2025 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 .
*
* 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 ajax */
let isConfigured = false;
/** @type {number} */
let defaultTimeout;
/** @type {string} */
let apiUrl;
/** @type {Espo.Ajax~Handler} */
let beforeSend;
/** @type {Espo.Ajax~Handler} */
let onSuccess;
/** @type {Espo.Ajax~Handler} */
let onError;
/** @type {Espo.Ajax~Handler} */
let onTimeout;
/** @type {function()} */
let onOffline;
/**
* @callback Espo.Ajax~Handler
* @param {XMLHttpRequest} [xhr]
* @param {Object.} [options]
*/
/**
* Options.
*
* @typedef {Object} Espo.Ajax~Options
*
* @property {Number} [timeout] A timeout.
* @property {Object.} [headers] A request headers.
* @property {'json'|'text'} [dataType] A data type.
* @property {string} [contentType] A content type.
* @property {boolean} [resolveWithXhr] To resolve with `XMLHttpRequest`.
*/
const baseUrl = _utils.default.obtainBaseUrl();
// noinspection JSUnusedGlobalSymbols
/**
* Functions for API HTTP requests.
*/
const Ajax = Espo.Ajax = {
/**
* Request.
*
* @param {string} url An URL.
* @param {'GET'|'POST'|'PUT'|'DELETE'|'PATCH'|'OPTIONS'} method An HTTP method.
* @param {*} [data] Data.
* @param {Espo.Ajax~Options & Object.} [options] Options.
* @returns {AjaxPromise}
*/
request: function (url, method, data, options) {
options = options || {};
const timeout = 'timeout' in options ? options.timeout : defaultTimeout;
const contentType = options.contentType || 'application/json';
let body;
if (options.data && !data) {
data = options.data;
}
if (apiUrl) {
url = Espo.Utils.trimSlash(apiUrl) + '/' + url;
}
if (!['GET', 'OPTIONS'].includes(method) && data) {
body = data;
if (contentType === 'application/json' && typeof data !== 'string') {
body = JSON.stringify(data);
}
}
if (method === 'GET' && data) {
const part = _jquery.default.param(data);
url.includes('?') ? url += '&' : url += '?';
url += part;
}
const urlObj = new URL(baseUrl + url);
const xhr = new Xhr();
xhr.timeout = timeout;
xhr.open(method, urlObj);
xhr.setRequestHeader('Content-Type', contentType);
if (options.headers) {
for (const key in options.headers) {
xhr.setRequestHeader(key, options.headers[key]);
}
}
if (beforeSend) {
beforeSend(xhr, options);
}
const promiseWrapper = {};
const promise = new AjaxPromise((resolve, reject) => {
const onErrorGeneral = isTimeout => {
if (options.error) {
options.error(xhr, options);
}
reject(xhr, options);
if (isTimeout) {
if (onTimeout) {
onTimeout(xhr, options);
}
return;
}
if (xhr.status === 0 && !navigator.onLine && onOffline) {
onOffline();
return;
}
if (onError) {
onError(xhr, options);
}
};
xhr.ontimeout = () => onErrorGeneral(true);
xhr.onerror = () => onErrorGeneral();
xhr.onload = () => {
if (xhr.status >= 400) {
onErrorGeneral();
return;
}
let response = xhr.responseText;
if ((options.dataType || 'json') === 'json') {
try {
response = JSON.parse(xhr.responseText);
} catch (e) {
console.error('Could not parse API response.');
onErrorGeneral();
}
}
if (options.success) {
options.success(response);
}
onSuccess(xhr, options);
if (options.resolveWithXhr) {
response = xhr;
}
resolve(response);
};
xhr.send(body);
if (promiseWrapper.promise) {
promiseWrapper.promise.xhr = xhr;
return;
}
promiseWrapper.xhr = xhr;
});
promiseWrapper.promise = promise;
promise.xhr = promise.xhr || promiseWrapper.xhr;
return promise;
},
/**
* POST request.
*
* @param {string} url An URL.
* @param {*} [data] Data.
* @param {Espo.Ajax~Options & Object.} [options] Options.
* @returns {Promise & AjaxPromise}
*/
postRequest: function (url, data, options) {
if (data) {
data = JSON.stringify(data);
}
return /** @type {Promise & AjaxPromise} */Ajax.request(url, 'POST', data, options);
},
/**
* PATCH request.
*
* @param {string} url An URL.
* @param {*} [data] Data.
* @param {Espo.Ajax~Options & Object.} [options] Options.
* @returns {Promise & AjaxPromise}
*/
patchRequest: function (url, data, options) {
if (data) {
data = JSON.stringify(data);
}
return /** @type {Promise & AjaxPromise} */Ajax.request(url, 'PATCH', data, options);
},
/**
* PUT request.
*
* @param {string} url An URL.
* @param {*} [data] Data.
* @param {Espo.Ajax~Options & Object.} [options] Options.
* @returns {Promise & AjaxPromise}
*/
putRequest: function (url, data, options) {
if (data) {
data = JSON.stringify(data);
}
return /** @type {Promise & AjaxPromise} */Ajax.request(url, 'PUT', data, options);
},
/**
* DELETE request.
*
* @param {string} url An URL.
* @param {*} [data] Data.
* @param {Espo.Ajax~Options & Object.} [options] Options.
* @returns {Promise & AjaxPromise}
*/
deleteRequest: function (url, data, options) {
if (data) {
data = JSON.stringify(data);
}
return /** @type {Promise & AjaxPromise} */Ajax.request(url, 'DELETE', data, options);
},
/**
* GET request.
*
* @param {string} url An URL.
* @param {*} [data] Data.
* @param {Espo.Ajax~Options & Object.