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

This commit is contained in:
upstream-mirror
2026-09-18 04:19:15 +05:00
commit 7cc7e6b4f3
10077 changed files with 900756 additions and 0 deletions

508
client/src/acl-manager.js Normal file
View File

@@ -0,0 +1,508 @@
/************************************************************************
* 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.
************************************************************************/
/** @module acl-manager */
import Acl from 'acl';
import Utils from 'utils';
import {View as BullView} from 'bullbone';
/**
* An action.
*
* @typedef {'create'|'read'|'edit'|'delete'|'stream'} module:acl-manager~action
*/
/**
* An access checking class for a specific scope.
*/
class AclManager {
/** @protected */
data = null
fieldLevelList = ['yes', 'no']
/**
* @param {module:models/user} user A user.
* @param {Object} implementationClassMap `acl` implementations.
* @param {boolean} aclAllowDeleteCreated Allow a user to delete records they created regardless a
* role access level.
*/
constructor(user, implementationClassMap, aclAllowDeleteCreated) {
this.setEmpty();
/** @protected */
this.user = user || null;
this.implementationClassMap = implementationClassMap || {};
this.aclAllowDeleteCreated = aclAllowDeleteCreated;
}
/**
* @protected
*/
setEmpty() {
this.data = {
table: {},
fieldTable: {},
fieldTableQuickAccess: {},
};
this.implementationHash = {};
this.forbiddenFieldsCache = {};
this.implementationClassMap = {};
this.forbiddenAttributesCache = {};
}
/**
* Get an `acl` implementation.
*
* @protected
* @param {string} scope A scope.
* @returns {module:acl}
*/
getImplementation(scope) {
if (!(scope in this.implementationHash)) {
let implementationClass = Acl;
if (scope in this.implementationClassMap) {
implementationClass = this.implementationClassMap[scope];
}
const forbiddenFieldList = this.getScopeForbiddenFieldList(scope);
const params = {
aclAllowDeleteCreated: this.aclAllowDeleteCreated,
teamsFieldIsForbidden: forbiddenFieldList.includes('teams'),
forbiddenFieldList: forbiddenFieldList,
};
this.implementationHash[scope] = new implementationClass(this.getUser(), scope, params, this);
}
return this.implementationHash[scope];
}
/**
* @return {import('models/user').default}
* @protected
*/
getUser() {
return this.user;
}
/**
* @internal
*/
set(data) {
data = data || {};
this.data = data;
this.data.table = this.data.table || {};
this.data.fieldTable = this.data.fieldTable || {};
this.data.attributeTable = this.data.attributeTable || {};
}
/**
* @deprecated Use `getPermissionLevel`.
*
* @returns {string|null}
*/
get(name) {
return this.data[name] || null;
}
/**
* Get a permission level.
*
* @param {string} permission A permission name.
* @returns {'yes'|'all'|'team'|'no'}
*/
getPermissionLevel(permission) {
let permissionKey = permission;
if (permission.slice(-10) !== 'Permission') {
permissionKey = permission + 'Permission';
}
return this.data[permissionKey] || 'no';
}
/**
* Get access level to a scope action.
*
* @param {string} scope A scope.
* @param {module:acl-manager~action} action An action.
* @returns {'yes'|'all'|'team'|'own'|'no'|null}
*/
getLevel(scope, action) {
if (!(scope in this.data.table)) {
return null;
}
const scopeItem = this.data.table[scope];
if (
typeof scopeItem !== 'object' ||
!(action in scopeItem)
) {
return null;
}
return scopeItem[action];
}
/**
* Clear access data.
*
* @internal
*/
clear() {
this.setEmpty();
}
/**
* Check whether a scope has ACL.
*
* @param {string} scope A scope.
* @returns {boolean}
*/
checkScopeHasAcl(scope) {
const data = (this.data.table || {})[scope];
if (typeof data === 'undefined') {
return false;
}
return true;
}
/**
* Check access to a scope.
*
* @param {string} scope A scope.
* @param {module:acl-manager~action|null} [action=null] An action.
* @param {boolean} [precise=false] Deprecated. Not used.
* @returns {boolean} True if access allowed.
*/
checkScope(scope, action, precise) {
let data = (this.data.table || {})[scope];
if (typeof data === 'undefined') {
data = null;
}
return this.getImplementation(scope).checkScope(data, action, precise);
}
/**
* Check access to a model.
*
* @param {module:model} model A model.
* @param {module:acl-manager~action|null} [action=null] An action.
* @param {boolean} [precise=false] To return `null` if not enough data is set in a model.
* E.g. the `teams` field is not yet loaded.
* @returns {boolean|null} True if access allowed, null if not enough data to determine.
*/
checkModel(model, action, precise) {
const scope = model.entityType;
// todo move this to custom acl
if (action === 'edit') {
if (!model.isEditable()) {
return false;
}
}
if (action === 'delete') {
if (!model.isRemovable()) {
return false;
}
}
let data = (this.data.table || {})[scope];
if (typeof data === 'undefined') {
data = null;
}
const impl = this.getImplementation(scope);
if (action) {
const methodName = 'checkModel' + Utils.upperCaseFirst(action);
if (methodName in impl) {
return impl[methodName](model, data, precise);
}
}
return impl.checkModel(model, data, action, precise);
}
/**
* Check access to a scope or a model.
*
* @param {string|module:model} subject What to check. A scope or a model.
* @param {module:acl-manager~action|null} [action=null] An action.
* @param {boolean} [precise=false] To return `null` if not enough data is set in a model.
* E.g. the `teams` field is not yet loaded.
* @returns {boolean|null} True if access allowed, null if not enough data to determine.
*/
check(subject, action, precise) {
if (typeof subject === 'string') {
return this.checkScope(subject, action, precise);
}
if (!subject) {
throw new Error("No model.");
}
return this.checkModel(subject, action, precise);
}
/**
* Check if a user is owner to a model.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if owner, null if not clear.
*/
checkIsOwner(model) {
return this.getImplementation(model.entityType).checkIsOwner(model);
}
// noinspection JSUnusedGlobalSymbols
/**
* Check if a user in a team of a model.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if in a team, null if not clear.
*/
checkInTeam(model) {
return this.getImplementation(model.entityType).checkInTeam(model);
}
// noinspection JSUnusedGlobalSymbols
/**
* Check if a record is shared with the user.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if shared, null if not clear.
*/
checkIsShared(model) {
return this.getImplementation(model.entityType).checkIsShared(model);
}
// noinspection JSUnusedGlobalSymbols
/**
* Check an assignment permission to a user.
*
* @param {module:models/user} user A user.
* @returns {boolean} True if access allowed.
*/
checkAssignmentPermission(user) {
return this.checkPermission('assignmentPermission', user);
}
/**
* Check a user permission to a user.
*
* @param {module:models/user} user A user.
* @returns {boolean} True if access allowed.
*/
checkUserPermission(user) {
return this.checkPermission('userPermission', user);
}
/**
* Check a specific permission to a user.
*
* @param {string} permission A permission name.
* @param {module:models/user} user A user.
* @returns {boolean|null} True if access allowed. Null if not enough data loaded to know for sure.
*/
checkPermission(permission, user) {
if (this.getUser().isAdmin()) {
return true;
}
const level = this.getPermissionLevel(permission);
if (level === 'no') {
if (user.id === this.getUser().id) {
return true;
}
return false;
}
if (level === 'team') {
if (!user.has('teamsIds')) {
return null;
}
let result = false;
const teamsIds = user.get('teamsIds') || [];
teamsIds.forEach(id => {
if ((this.getUser().get('teamsIds') || []).includes(id)) {
result = true;
}
});
return result;
}
if (level === 'all') {
return true;
}
if (level === 'yes') {
return true;
}
return false;
}
/**
* Get a list of forbidden fields for an entity type.
*
* @param {string} scope An entity type.
* @param {'read'|'edit'} [action='read'] An action.
* @param {'yes'|'no'} [thresholdLevel='no'] A threshold level.
* @returns {string[]} A forbidden field list.
*/
getScopeForbiddenFieldList(scope, action, thresholdLevel) {
action = action || 'read';
thresholdLevel = thresholdLevel || 'no';
const key = scope + '_' + action + '_' + thresholdLevel;
if (key in this.forbiddenFieldsCache) {
return Utils.clone(this.forbiddenFieldsCache[key]);
}
const levelList = this.fieldLevelList.slice(this.fieldLevelList.indexOf(thresholdLevel));
const fieldTableQuickAccess = this.data.fieldTableQuickAccess || {};
const scopeData = fieldTableQuickAccess[scope] || {};
const fieldsData = scopeData.fields || {};
const actionData = fieldsData[action] || {};
const fieldList = [];
levelList.forEach(level => {
const list = actionData[level] || [];
list.forEach(field => {
if (fieldList.includes(field)) {
return;
}
fieldList.push(field);
});
});
this.forbiddenFieldsCache[key] = fieldList;
return Utils.clone(fieldList);
}
/**
* Get a list of forbidden attributes for an entity type.
*
* @param {string} scope An entity type.
* @param {'read'|'edit'} [action='read'] An action.
* @param {'yes'|'no'} [thresholdLevel='no'] A threshold level.
* @returns {string[]} A forbidden attribute list.
*/
getScopeForbiddenAttributeList(scope, action, thresholdLevel) {
action = action || 'read';
thresholdLevel = thresholdLevel || 'no';
const key = scope + '_' + action + '_' + thresholdLevel;
if (key in this.forbiddenAttributesCache) {
return Utils.clone(this.forbiddenAttributesCache[key]);
}
const levelList = this.fieldLevelList.slice(this.fieldLevelList.indexOf(thresholdLevel));
const fieldTableQuickAccess = this.data.fieldTableQuickAccess || {};
const scopeData = fieldTableQuickAccess[scope] || {};
const attributesData = scopeData.attributes || {};
const actionData = attributesData[action] || {};
const attributeList = [];
levelList.forEach(level => {
const list = actionData[level] || [];
list.forEach(attribute => {
if (attributeList.includes(attribute)) {
return;
}
attributeList.push(attribute);
});
});
this.forbiddenAttributesCache[key] = attributeList;
return Utils.clone(attributeList);
}
/**
* Check an assignment permission to a team.
*
* @param {string} teamId A team ID.
* @returns {boolean} True if access allowed.
*/
checkTeamAssignmentPermission(teamId) {
if (this.getPermissionLevel('assignmentPermission') === 'all') {
return true;
}
return this.getUser().getLinkMultipleIdList('teams').includes(teamId);
}
/**
* Check access to a field.
* @param {string} scope An entity type.
* @param {string} field A field.
* @param {'read'|'edit'} [action='read'] An action.
* @returns {boolean} True if access allowed.
*/
checkField(scope, field, action) {
return !this.getScopeForbiddenFieldList(scope, action).includes(field);
}
}
AclManager.extend = BullView.extend;
export default AclManager;

View File

@@ -0,0 +1,91 @@
/************************************************************************
* 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.
************************************************************************/
/** @module acl-portal-manager */
import AclManager from 'acl-manager';
import AclPortal from 'acl-portal';
/**
* An access checking class for a specific scope for portals.
*/
class AclPortalManager extends AclManager {
// noinspection JSUnusedGlobalSymbols
/**
* Check if a user in an account of a model.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if in an account, null if not clear.
*/
checkInAccount(model) {
const impl = /** @type {module:acl-portal} */this.getImplementation(model.entityType);
return impl.checkInAccount(model);
}
// noinspection JSUnusedGlobalSymbols
/**
* Check if a user is a contact-owner to a model.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if in a contact-owner, null if not clear.
*/
checkIsOwnContact(model) {
const impl =
/** @type {module:acl-portal} */
this.getImplementation(model.entityType);
return impl.checkIsOwnContact(model);
}
/**
* @param {string} scope A scope.
* @returns {module:acl-portal}
*/
getImplementation(scope) {
if (!(scope in this.implementationHash)) {
let implementationClass = AclPortal;
if (scope in this.implementationClassMap) {
implementationClass = this.implementationClassMap[scope];
}
const params = {
aclAllowDeleteCreated: false,
forbiddenFieldList: this.getScopeForbiddenFieldList(scope),
};
this.implementationHash[scope] = new implementationClass(this.getUser(), scope, params, this);
}
return this.implementationHash[scope];
}
}
export default AclPortalManager;

394
client/src/acl-portal.js Normal file
View File

@@ -0,0 +1,394 @@
/************************************************************************
* 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.
************************************************************************/
/** @module acl-portal */
import Acl from 'acl';
import {inject} from 'di';
import Metadata from 'metadata';
/**
* Internal class for portal access checking. Can be extended to customize access checking
* for a specific scope.
*/
class AclPortal extends Acl {
/**
* @private
* @type {Metadata}
*/
@inject(Metadata)
metadata
/** @inheritDoc */
checkScope(data, action, precise, entityAccessData) {
entityAccessData = entityAccessData || {};
const inAccount = entityAccessData.inAccount;
const isOwnContact = entityAccessData.isOwnContact;
const isOwner = entityAccessData.isOwner;
if (this.getUser().isAdmin()) {
return true;
}
if (data === false) {
return false;
}
if (data === true) {
return true;
}
if (typeof data === 'string') {
return true;
}
if (data === null) {
return false;
}
action = action || null;
if (action === null) {
return true;
}
if (!(action in data)) {
return false;
}
const value = data[action];
if (value === 'all') {
return true;
}
if (value === 'yes') {
return true;
}
if (value === 'no') {
return false;
}
if (typeof isOwner === 'undefined') {
return true;
}
if (isOwner && (value === 'own' || value === 'account' || value === 'contact')) {
return true;
}
let result = false;
if (value === 'account') {
if (inAccount === null) {
if (!precise) {
return true;
}
result = null;
} else if (inAccount) {
return true;
}
if (isOwnContact === null) {
if (!precise) {
return true;
}
result = null;
} else if (isOwnContact) {
return true;
}
}
if (value === 'contact') {
if (isOwnContact === null) {
if (!precise) {
return true;
}
result = null;
} else if (isOwnContact) {
return true;
}
}
if (isOwner === null) {
if (!precise) {
return true;
}
result = null;
}
return result;
}
/** @inheritDoc */
checkModel(model, data, action, precise) {
if (this.getUser().isAdmin()) {
return true;
}
const entityAccessData = {
isOwner: this.checkIsOwner(model),
inAccount: this.checkInAccount(model),
isOwnContact: this.checkIsOwnContact(model),
};
return this.checkScope(data, action, precise, entityAccessData);
}
/** @inheritDoc */
checkIsOwner(model) {
if (
model.hasField('createdBy') &&
this.getUser().id === model.get('createdById')
) {
return true;
}
return false;
}
/**
* Check if a user in an account of a model.
*
* @param {import('model').default} model A model.
* @returns {boolean|null} True if in an account, null if not clear.
*/
checkInAccount(model) {
const accountsIds = this.getUser().getLinkMultipleIdList('accounts');
if (!accountsIds.length) {
return false;
}
const link = this.metadata.get(`aclDefs.${model.entityType}.accountLink`);
if (link) {
const linkType = model.getLinkType(link);
if (linkType === 'belongsTo' || linkType === 'hasOne') {
const idAttribute = link + 'Id';
if (!model.has(idAttribute)) {
return null;
}
const id = model.get(idAttribute);
if (!id) {
return false;
}
return accountsIds.includes(id);
}
if (linkType === 'belongsToParent') {
const idAttribute = link + 'Id';
const typeAttribute = link + 'Type';
if (!model.has(idAttribute) || !model.has(typeAttribute)) {
return null;
}
const id = model.get(idAttribute);
if (model.get(typeAttribute) !== 'Account' || !id) {
return false;
}
return accountsIds.includes(id);
}
if (linkType === 'hasMany') {
if (!model.hasField(link) || model.getFieldType(link) !== 'linkMultiple') {
return true;
}
if (!model.has(link + 'Ids')) {
return null;
}
const ids = model.getLinkMultipleIdList(link);
for (const id of ids) {
if (accountsIds.includes(id)) {
return true;
}
}
return false;
}
return false;
}
if (
model.hasField('account') &&
model.get('accountId') &&
accountsIds.includes(model.get('accountId'))
) {
return true;
}
let result = false;
if (model.hasField('accounts') && model.hasLink('accounts')) {
if (!model.has('accountsIds')) {
result = null;
}
(model.getLinkMultipleIdList('accounts')).forEach(id => {
if (accountsIds.includes(id)) {
result = true;
}
});
}
if (
model.hasField('parent') &&
model.hasLink('parent') &&
model.get('parentType') === 'Account' &&
accountsIds.includes(model.get('parentId'))
) {
return true;
}
if (result === false) {
if (!model.hasField('accounts') && model.hasLink('accounts')) {
return true;
}
}
return result;
}
/**
* Check if a user is a contact-owner to a model.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if in a contact-owner, null if not clear.
*/
checkIsOwnContact(model) {
const contactId = this.getUser().get('contactId');
if (!contactId) {
return false;
}
const link = this.metadata.get(`aclDefs.${model.entityType}.contactLink`);
if (link) {
const linkType = model.getLinkType(link);
if (linkType === 'belongsTo' || linkType === 'hasOne') {
const idAttribute = link + 'Id';
if (!model.has(idAttribute)) {
return null;
}
return model.get(idAttribute) === contactId;
}
if (linkType === 'belongsToParent') {
const idAttribute = link + 'Id';
const typeAttribute = link + 'Type';
if (!model.has(idAttribute) || !model.has(typeAttribute)) {
return null;
}
if (model.get(typeAttribute) !== 'Contact') {
return false;
}
return model.get(idAttribute) === contactId;
}
if (linkType === 'hasMany') {
if (!model.hasField(link) || model.getFieldType(link) !== 'linkMultiple') {
return true;
}
if (!model.has(link + 'Ids')) {
return null;
}
const ids = model.getLinkMultipleIdList(link);
return ids.includes(contactId);
}
return false;
}
if (model.hasField('contact')) {
if (model.get('contactId')) {
if (contactId === model.get('contactId')) {
return true;
}
}
}
let result = false;
if (model.hasField('contacts') && model.hasLink('contacts')) {
if (!model.has('contactsIds')) {
result = null;
}
(model.getLinkMultipleIdList('contacts')).forEach(id => {
if (contactId === id) {
result = true;
}
});
}
if (model.hasField('parent') && model.hasLink('parent')) {
if (model.get('parentType') === 'Contact' && model.get('parentId') === contactId) {
return true;
}
}
if (result === false) {
if (!model.hasField('contacts') && model.hasLink('contacts')) {
return true;
}
}
return result;
}
}
export default AclPortal;

View File

@@ -0,0 +1,64 @@
/************************************************************************
* 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 AclPortal from 'acl-portal';
class EmailAclPortal extends AclPortal {
// noinspection JSUnusedGlobalSymbols
checkModelRead(model, data, precise) {
const result = this.checkModel(model, data, 'read', precise);
if (result) {
return true;
}
if (data === false) {
return false;
}
const d = data || {};
if (d.read === 'no') {
return false;
}
if (model.has('usersIds')) {
if (~(model.get('usersIds') || []).indexOf(this.getUser().id)) {
return true;
}
}
else if (precise) {
return null;
}
return result;
}
}
export default EmailAclPortal;

View File

@@ -0,0 +1,42 @@
/************************************************************************
* 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 AclPortal from 'acl-portal';
class NotificationAclPortal extends AclPortal {
checkIsOwner(model) {
if (this.getUser().id === model.get('userId')) {
return true;
}
return false;
}
}
export default NotificationAclPortal;

View File

@@ -0,0 +1,42 @@
/************************************************************************
* 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 AclPortal from 'acl-portal';
class PreferencesAclPortal extends AclPortal {
checkIsOwner(model) {
if (this.getUser().id === model.id) {
return true;
}
return false;
}
}
export default PreferencesAclPortal;

384
client/src/acl.js Normal file
View File

@@ -0,0 +1,384 @@
/************************************************************************
* 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.
************************************************************************/
/** @module acl */
import {View as BullView} from 'bullbone';
/**
* Internal class for access checking. Can be extended to customize access checking
* for a specific scope.
*/
class Acl {
/**
* @param {module:models/user} user A user.
* @param {string} scope A scope.
* @param {Object} params Parameters.
* @param {import('acl-manager').default} aclManager
*/
constructor(user, scope, params, aclManager) {
/**
* A user.
*
* @type {module:models/user|null}
* @protected
*/
this.user = user || null;
this.scope = scope;
params = params || {};
this.aclAllowDeleteCreated = params.aclAllowDeleteCreated;
this.teamsFieldIsForbidden = params.teamsFieldIsForbidden;
/**
* @type {string[]}
*/
this.forbiddenFieldList = params.forbiddenFieldList || [];
/**
* @protected
* @type {boolean}
*/
this.collaboratorsFieldIsForbidden = this.forbiddenFieldList.includes('collaborators');
/**
* @type {import('acl-manager').default}
* @private
*/
this._aclManager = aclManager;
}
/**
* Get a user.
*
* @returns {module:models/user}
* @protected
*/
getUser() {
return this.user;
}
/**
* Check access to a scope.
*
* @param {string|boolean|Object.<string, string>} data Access data.
* @param {module:acl-manager~action|null} [action=null] An action.
* @param {boolean} [precise=false] To return `null` if `inTeam == null`.
* @param {Record.<string, boolean|null>|null} [entityAccessData=null] Entity access data. `inTeam`, `isOwner`.
* @returns {boolean|null} True if access allowed.
*/
checkScope(data, action, precise, entityAccessData) {
entityAccessData = entityAccessData || {};
const inTeam = entityAccessData.inTeam;
const isOwner = entityAccessData.isOwner;
const isShared = entityAccessData.isShared;
if (this.getUser().isAdmin()) {
if (data === false) {
return false;
}
return true;
}
if (data === false) {
return false;
}
if (data === true) {
return true;
}
if (typeof data === 'string') {
return true;
}
if (data === null) {
return false;
}
action = action || null;
if (action === null) {
return true;
}
if (!(action in data)) {
return false;
}
const value = data[action];
if (value === 'all') {
return true;
}
if (value === 'yes') {
return true;
}
if (value === 'no') {
return false;
}
if (isOwner === undefined) {
return true;
}
if (isOwner) {
if (value === 'own' || value === 'team') {
return true;
}
}
if (isShared) {
return true;
}
if (inTeam) {
if (value === 'team') {
return true;
}
}
let result = false;
if (value === 'team') {
if (inTeam === null && precise) {
result = null;
}
}
if (isOwner === null && precise) {
result = null;
}
if (isShared === null) {
result = null;
}
return result;
}
/**
* Check access to model (entity).
*
* @param {module:model} model A model.
* @param {Object.<string, string>|string|null} data Access data.
* @param {module:acl-manager~action|null} [action=null] Action to check.
* @param {boolean} [precise=false] To return `null` if not enough data is set in a model.
* E.g. the `teams` field is not yet loaded.
* @returns {boolean|null} True if access allowed, null if not enough data to determine.
*/
checkModel(model, data, action, precise) {
if (this.getUser().isAdmin()) {
return true;
}
let isShared = false;
if (action === 'read' || action === 'stream') {
isShared = this.checkIsShared(model);
}
const entityAccessData = {
isOwner: this.checkIsOwner(model),
inTeam: this.checkInTeam(model),
isShared: isShared,
};
return this.checkScope(data, action, precise, entityAccessData);
}
// noinspection JSUnusedGlobalSymbols
/**
* Check `delete` access to model.
*
* @param {module:model} model A model.
* @param {Object.<string, string>|string|null} data Access data.
* @param {boolean} [precise=false] To return `null` if not enough data is set in a model.
* E.g. the `teams` field is not yet loaded.
* @returns {boolean} True if access allowed.
*/
checkModelDelete(model, data, precise) {
const result = this.checkModel(model, data, 'delete', precise);
if (result) {
return true;
}
if (data === false) {
return false;
}
const d = data || {};
if (d.read === 'no') {
return false;
}
if (
model.has('createdById') &&
model.get('createdById') === this.getUser().id &&
this.aclAllowDeleteCreated
) {
if (!model.has('assignedUserId')) {
return true;
}
if (!model.get('assignedUserId')) {
return true;
}
if (model.get('assignedUserId') === this.getUser().id) {
return true;
}
}
return result;
}
/**
* Check if a user is owner to a model.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if owner. Null if not clear.
*/
checkIsOwner(model) {
let result = false;
if (model.hasField('assignedUser')) {
if (this.getUser().id === model.get('assignedUserId')) {
return true;
}
if (!model.has('assignedUserId')) {
result = null;
}
}
else if (model.hasField('createdBy')) {
if (this.getUser().id === model.get('createdById')) {
return true;
}
if (!model.has('createdById')) {
result = null;
}
}
if (model.hasField('assignedUsers')) {
if (!model.has('assignedUsersIds')) {
return null;
}
if ((model.get('assignedUsersIds') || []).includes(this.getUser().id)) {
return true;
}
result = false;
}
return result;
}
/**
* Check if a user in a team of a model.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if in a team. Null if not enough data to determine.
*/
checkInTeam(model) {
const userTeamIdList = this.getUser().getTeamIdList();
if (!model.has('teamsIds')) {
if (this.teamsFieldIsForbidden) {
return true;
}
if (!model.hasField('teams')) {
return false;
}
return null;
}
const teamIdList = model.getTeamIdList();
let inTeam = false;
userTeamIdList.forEach(id => {
if (teamIdList.includes(id)) {
inTeam = true;
}
});
return inTeam;
}
/**
* Check if a record is shared with the user.
*
* @param {module:model} model A model.
* @returns {boolean|null} True if shared. Null if not enough data to determine.
*/
checkIsShared(model) {
if (!model.has('collaboratorsIds')) {
if (this.collaboratorsFieldIsForbidden) {
return true;
}
if (!model.hasField('collaborators')) {
return false;
}
return null;
}
const collaboratorsIds = model.getLinkMultipleIdList('collaborators');
return collaboratorsIds.includes(this.user.id);
}
/**
* Get a permission level.
*
* @protected
* @param {string} permission A permission name.
* @returns {'yes'|'all'|'team'|'no'}
*/
getPermissionLevel(permission) {
return this._aclManager.getPermissionLevel(permission);
}
}
Acl.extend = BullView.extend;
export default Acl;

View File

@@ -0,0 +1,40 @@
/************************************************************************
* 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 Acl from 'acl';
export default class extends Acl {
checkScope(data, action, precise, entityAccessData) {
if (action === 'create' || action === 'delete') {
action = 'edit';
}
return super.checkScope(data, action, precise, entityAccessData);
}
}

121
client/src/acl/email.js Normal file
View File

@@ -0,0 +1,121 @@
/************************************************************************
* 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 Acl from 'acl';
class EmailAcl extends Acl {
// noinspection JSUnusedGlobalSymbols
checkModelRead(model, data, precise) {
const result = this.checkModel(model, data, 'read', precise);
if (result) {
return true;
}
if (data === false) {
return false;
}
const d = data || {};
if (d.read === 'no') {
return false;
}
if (model.has('usersIds')) {
if (~(model.get('usersIds') || []).indexOf(this.getUser().id)) {
return true;
}
}
else if (precise) {
return null;
}
return result;
}
checkIsOwner(model) {
if (
this.getUser().id === model.get('assignedUserId') ||
this.getUser().id === model.get('createdById')
) {
return true;
}
if (!model.has('assignedUsersIds')) {
return null;
}
if (~(model.get('assignedUsersIds') || []).indexOf(this.getUser().id)) {
return true;
}
return false;
}
// noinspection JSUnusedGlobalSymbols
checkModelEdit(model, data, precise) {
if (
model.get('status') === 'Draft' &&
model.get('createdById') === this.getUser().id
) {
return true;
}
return this.checkModel(model, data, 'edit', precise);
}
checkModelDelete(model, data, precise) {
const result = this.checkModel(model, data, 'delete', precise);
if (result) {
return true;
}
if (data === false) {
return false;
}
const d = data || {};
if (d.read === 'no') {
return false;
}
if (model.get('createdById') === this.getUser().id) {
if (model.get('status') !== 'Sent' && model.get('status') !== 'Archived') {
return true;
}
}
return result;
}
}
export default EmailAcl;

45
client/src/acl/foreign.js Normal file
View File

@@ -0,0 +1,45 @@
/************************************************************************
* 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 Acl from 'acl';
/**
* To be used for entities for which access is determined by access to a foreign record.
*/
class ForeignAcl extends Acl {
checkIsOwner(model) {
return true;
}
checkInTeam(model) {
return true;
}
}
export default ForeignAcl;

55
client/src/acl/import.js Normal file
View File

@@ -0,0 +1,55 @@
/************************************************************************
* 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 Acl from 'acl';
class ImportAcl extends Acl {
checkScope(data, action, precise, entityAccessData) {
return !!data;
}
// noinspection JSUnusedGlobalSymbols,JSUnusedLocalSymbols
checkModelRead(model, data, precise) {
return true;
}
checkIsOwner(model) {
if (this.getUser().id === model.get('createdById')) {
return true;
}
return false;
}
checkModelDelete(model, data, precise) {
return true;
}
}
export default ImportAcl;

View File

@@ -0,0 +1,42 @@
/************************************************************************
* 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 Acl from 'acl';
class NotificationAcl extends Acl {
checkIsOwner(model) {
if (this.getUser().id === model.get('userId')) {
return true;
}
return false;
}
}
export default NotificationAcl;

View File

@@ -0,0 +1,42 @@
/************************************************************************
* 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 Acl from 'acl';
class PreferencesAcl extends Acl {
checkIsOwner(model) {
if (this.getUser().id === model.id) {
return true;
}
return false;
}
}
export default PreferencesAcl;

40
client/src/acl/team.js Normal file
View File

@@ -0,0 +1,40 @@
/************************************************************************
* 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 Acl from 'acl';
class TeamAcl extends Acl {
checkInTeam(model) {
const userTeamIdList = this.getUser().getTeamIdList();
return (userTeamIdList.indexOf(model.id) !== -1);
}
}
export default TeamAcl;

50
client/src/acl/user.js Normal file
View File

@@ -0,0 +1,50 @@
/************************************************************************
* 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 Acl from 'acl';
class UserAcl extends Acl {
// noinspection JSUnusedGlobalSymbols
checkModelRead(model, data, precise) {
if (
model.isPortal() &&
this.getPermissionLevel('portalPermission') === 'yes'
) {
return true;
}
return this.checkModel(model, data, 'read', precise);
}
checkIsOwner(model) {
return this.getUser().id === model.id;
}
}
export default UserAcl;

View File

@@ -0,0 +1,52 @@
/************************************************************************
* 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.
************************************************************************/
/** @module action-handler */
import {View as BullView} from 'bullbone';
/**
* An action handler. To be extended by specific action handlers.
* @todo Use template of TView. Convert to TS.
*/
class ActionHandler {
/**
* @param {import('view').default} view A view.
*/
constructor(view) {
/**
* @protected
*/
this.view = view;
}
}
ActionHandler.extend = BullView.extend;
export default ActionHandler;

355
client/src/ajax.ts Normal file
View File

@@ -0,0 +1,355 @@
/************************************************************************
* 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.
************************************************************************/
/** @module ajax */
import $ from 'jquery';
import Utils from 'utils';
import {AjaxPromise, Xhr} from 'util/ajax';
let isConfigured: boolean = false;
let defaultTimeout: number;
let apiUrl: string;
let beforeSend: Handler;
let onSuccess: Handler;
let onError: Handler;
let onTimeout: Handler;
let onOffline: (() => void) | undefined;
type Handler = (xhr?: XMLHttpRequest, options?: Record<string, any>) => void;
/**
* Options.
*/
interface Options {
timeout?: number,
headers?: Record<string, string>,
dataType?: 'json' | 'text',
contentType?: string,
resolveWithXhr?: boolean,
}
const baseUrl = Utils.obtainBaseUrl();
// noinspection JSUnusedGlobalSymbols
/**
* Functions for API HTTP requests.
*/
const Ajax = {
/**
* Request.
*
* @param url An URL.
* @param method An HTTP method.
* @param {*} [data] Data.
* @param [options] Options.
* @returns {AjaxPromise<any>}
*/
request: function (
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS',
data: any = undefined,
options: Options & Record<string, any> = {}
): AjaxPromise {
const timeout = 'timeout' in options ? options.timeout : defaultTimeout;
const contentType = options.contentType || 'application/json';
let body: any;
if (options.data && !data) {
data = options.data;
}
if (apiUrl) {
url = 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 = $.param(data);
url.includes('?') ?
url += '&' :
url += '?';
url += part;
}
const urlObj = new URL(baseUrl + url);
const xhr = new Xhr();
if (timeout != null) {
xhr.timeout = timeout;
}
xhr.open(method, urlObj);
const headers = options.headers ?? {}
const hasContentType = Object.keys(headers).find(it => it.toLowerCase() === 'content-type');
if (!hasContentType) {
xhr.setRequestHeader('Content-Type', contentType);
}
for (const key in headers) {
xhr.setRequestHeader(key, headers[key]);
}
if (beforeSend) {
beforeSend(xhr, options);
}
const promiseWrapper: {
promise?: AjaxPromise,
xhr?: Xhr,
} = {};
const promise = new AjaxPromise<any>((resolve, reject) => {
const onErrorGeneral = (isTimeout: boolean = false) => {
if (options.error) {
options.error(xhr, options);
}
// @ts-ignore
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: string | Xhr = 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 ?? null;
return promise;
},
/**
* POST request.
*
* @param {string} url An URL.
* @param [data] Data.
* @param [options] Options.
*/
postRequest: function (
url: string,
data?: any,
options?: Options & Record<string, any>,
): Promise<any> & AjaxPromise {
if (data) {
data = JSON.stringify(data);
}
return Ajax.request(url, 'POST', data, options);
},
/**
* PATCH request.
*
* @param url An URL.
* @param [data] Data.
* @param [options] Options.
*/
patchRequest: function (
url: string,
data: any = undefined,
options?: Options & Record<string, any>,
): Promise<any> & AjaxPromise {
if (data) {
data = JSON.stringify(data);
}
return Ajax.request(url, 'PATCH', data, options);
},
/**
* PUT request.
*
* @param url An URL.
* @param [data] Data.
* @param [options] Options.
*/
putRequest: function (
url: string,
data: any = undefined,
options?: Options & Record<string, any>,
): Promise<any> & AjaxPromise {
if (data) {
data = JSON.stringify(data);
}
return Ajax.request(url, 'PUT', data, options);
},
/**
* DELETE request.
*
* @param url An URL.
* @param [data] Data.
* @param [options] Options.
*/
deleteRequest: function (
url: string,
data?: any,
options?: Options & Record<string, any>,
): Promise<any> & AjaxPromise {
if (data) {
data = JSON.stringify(data);
}
return Ajax.request(url, 'DELETE', data, options);
},
/**
* GET request.
*
* @param url An URL.
* @param [data] Data.
* @param [options] Options.
*/
getRequest: function (
url: string,
data: any = undefined,
options?: Options & Record<string, any>,
): Promise<any> & AjaxPromise {
return Ajax.request(url, 'GET', data, options);
},
/**
* @internal
* @param options Options.
*/
configure: function (
options: {
apiUrl: string,
timeout: number,
beforeSend: Handler,
onSuccess: Handler,
onError: Handler,
onTimeout: Handler,
onOffline?: () => void,
}
) {
if (isConfigured) {
throw new Error("Ajax is already configured.");
}
apiUrl = options.apiUrl;
defaultTimeout = options.timeout;
beforeSend = options.beforeSend;
onSuccess = options.onSuccess;
onError = options.onError;
onTimeout = options.onTimeout;
onOffline = options.onOffline;
isConfigured = true;
},
};
// @ts-ignore
Espo.Ajax = Ajax;
export default Ajax;

73
client/src/app-params.js Normal file
View File

@@ -0,0 +1,73 @@
/************************************************************************
* 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.
************************************************************************/
/**
* Application parameters.
*
* @since 9.0.0
*/
export default class AppParams {
/**
* @param {Record} params
*/
constructor(params = {}) {
/** @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/appParams');
this.params = data;
}
}

51
client/src/app-portal.js Normal file
View File

@@ -0,0 +1,51 @@
/************************************************************************
* 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.
************************************************************************/
/** @module app-portal */
import App from 'app';
import AclPortalManager from 'acl-portal-manager';
/**
* A portal application class.
*/
class AppPortal extends App {
aclName = 'aclPortal'
masterView = 'views/site-portal/master'
createAclManager() {
return new AclPortalManager(
this.user,
null,
this.settings.get('aclAllowDeleteCreated')
);
}
}
export default AppPortal

1639
client/src/app.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,74 @@
/************************************************************************
* 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.
************************************************************************/
/** @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);
}
}
export default BroadcastChannel;

222
client/src/cache.js Normal file
View File

@@ -0,0 +1,222 @@
/************************************************************************
* 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.
************************************************************************/
/** @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];
}
}
}
}
export default Cache;

View File

@@ -0,0 +1,100 @@
/************************************************************************
* 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.
************************************************************************/
/** @module collection-factory */
import ModelFactory from 'model-factory';
import Settings from 'models/settings';
import Metadata from 'metadata';
import Collection from 'collection';
import Model from 'model';
/**
* A collection factory.
*/
export default class CollectionFactory {
private readonly recordListMaxSizeLimit: number
/**
* @param {module:model-factory} modelFactory
* @param {module:models/settings} config
* @param {module:metadata} metadata
*/
constructor(
private modelFactory: ModelFactory,
config: Settings,
private metadata: Metadata,
) {
this.recordListMaxSizeLimit = config.get('recordListMaxSizeLimit') || 200;
}
/**
* Create a collection.
*
* @param entityType An entity type.
* @param [callback] Deprecated.
* @param [context] Deprecated.
* @returns A created collection.
*/
create<T extends Model = Model>(
entityType: string,
callback?: Function, context?: object,
): Promise<Collection<T>> {
return new Promise(resolve => {
context = context || this;
this.modelFactory.getSeed(entityType, Model => {
const orderBy = this.metadata.get(['entityDefs', entityType, 'collection', 'orderBy']);
const order = this.metadata.get(['entityDefs', entityType, 'collection', 'order']);
const className = this.metadata.get(['clientDefs', entityType, 'collection']) || 'collection';
const defs = this.metadata.get(['entityDefs', entityType]) || {};
Espo.loader.require(className, (collectionClass: typeof Collection) => {
const collection = new collectionClass(null, {
entityType: entityType,
orderBy: orderBy,
order: order,
defs: defs,
model: Model,
});
collection.entityType = entityType;
collection.maxMaxSize = this.recordListMaxSizeLimit;
if (callback) {
callback.call(context, collection);
}
resolve(collection as Collection<T>);
});
});
});
}
}

1341
client/src/collection.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
/************************************************************************
* 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.
************************************************************************/
/** @module collections/note */
import Collection from 'collection';
class NoteCollection extends Collection {
/**
* @type {boolean}
*/
paginationByNumber = false
/**
* @private
* @type {string|null}
*/
reactionsCheckDate = null
/**
* @type {Record[]}
*/
pinnedList
/**
* @type {number}
*/
reactionsCheckMaxSize = 0;
prepareAttributes(response, params) {
if (Array.isArray(response)) {
throw new Error("Bad response.");
}
const total = this.total;
const list = super.prepareAttributes(response, params);
if (params.data && params.data.after) {
this.total = total >= 0 && response.total >= 0 ?
total + response.total : total;
}
if (response.pinnedList) {
this.pinnedList = Espo.Utils.cloneDeep(response.pinnedList);
}
this.reactionsCheckDate = response.reactionsCheckDate;
/** @type {Record[]} */
const updatedReactions = response.updatedReactions;
if (updatedReactions) {
updatedReactions.forEach(item => {
const model = this.get(item.id);
if (!model) {
return;
}
model.set(item);
});
}
return list;
}
/**
* Fetch new records.
*
* @param {Object} [options] Options.
* @returns {Promise}
*/
fetchNew(options) {
options = options || {};
options.data = options.data || {};
options.fetchNew = true;
options.noRebuild = true;
options.lengthBeforeFetch = this.length;
if (this.length) {
options.data.after = this.models[0].get('createdAt');
options.remove = false;
options.at = 0;
options.maxSize = null;
if (this.reactionsCheckMaxSize) {
options.data.reactionsAfter = this.reactionsCheckDate || options.data.after;
options.data.reactionsCheckNoteIds = this.models
.filter(model => model.attributes.type === 'Post')
.map(model => model.id)
.slice(0, this.reactionsCheckMaxSize)
.join(',');
}
}
return this.fetch(options);
}
fetch(options) {
options = {...options};
if (this.paginationByNumber && options.more) {
options.more = false;
options.data = options.data || {};
const lastModel = this.models.at(this.length - 1);
if (lastModel) {
options.data.beforeNumber = lastModel.get('number');
}
}
return super.fetch(options);
}
}
export default NoteCollection;

View File

@@ -0,0 +1,34 @@
/************************************************************************
* 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 NoteCollection from 'collections/note';
export default class NotificationCollection extends NoteCollection {
paginationByNumber = true
}

View File

@@ -0,0 +1,50 @@
/************************************************************************
* 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 Collection from 'collection';
import {CreatedSecrets} from 'helpers/misc/o-auth-client-secret';
// noinspection JSUnusedGlobalSymbols
export default class OAuthClientSecretCollection extends Collection {
protected prepareAttributes(
response: Record<string, any>[] | Record<string, any>,
options: Record<string, any>
): Record<string, any>[] {
const items = super.prepareAttributes(response, options);
for (const item of items) {
if (item.id && CreatedSecrets[item.id]) {
item.value = CreatedSecrets[item.id];
}
}
return items;
}
}

View File

@@ -0,0 +1,169 @@
/************************************************************************
* 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.
************************************************************************/
/** @module collections/tree */
import Collection from 'collection';
class TreeCollection extends Collection {
/**
* @type {string | null}
*/
parentId
/**
* @type {string|null}
*/
currentId = null
/**
* @type {string[]|null}
*/
path
/**
* @type {string[]|null}
*/
openPath
/**
* @internal
* @type {string | null}
*/
currentCategoryId
/**
* @internal
* @type {string | null}
*/
currentCategoryName
/**
* @return {TreeCollection}
*/
createSeed() {
const seed = new this.constructor();
seed.url = this.url;
seed.model = this.model;
seed.name = this.name;
seed.entityType = this.entityType;
seed.defs = this.defs;
return seed;
}
prepareAttributes(response, options) {
if (Array.isArray(response)) {
throw new Error("Bad response.");
}
const list = super.prepareAttributes(response, options);
const seed = this.clone();
seed.reset();
this.path = response.path;
this.openPath = response.openPath ?? null;
/**
* @type {{
* id: string,
* name: string,
* upperId?: string,
* upperName?: string,
* }|null}
*/
this.categoryData = response.data || null;
const prepare = (list, depth) => {
list.forEach(data => {
data.depth = depth;
const childCollection = this.createSeed();
childCollection.parentId = data.id;
if (data.childList) {
if (data.childList.length) {
prepare(data.childList, depth + 1);
childCollection.set(data.childList);
data.childCollection = childCollection;
return;
}
data.childCollection = childCollection;
return;
}
if (data.childList === null) {
data.childCollection = null;
return;
}
data.childCollection = childCollection;
});
};
prepare(list, 0);
return list;
}
fetch(options) {
options = options || {};
options.data = options.data || {};
if (this.parentId) {
options.data.parentId = this.parentId;
}
if (this.currentId) {
options.data.currentId = this.currentId;
}
return super.fetch(options);
}
clone(options = {}) {
options = {...options};
// Prevents recurring clone.
options.withModels = false;
return super.clone(options);
}
}
export default TreeCollection;

View File

@@ -0,0 +1,276 @@
/************************************************************************
* 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 Language from 'language';
import {inject} from 'di';
import {h, VNode, toVNode} from 'bullbone';
interface ButtonOptions {
name: string | null;
action?: string | null;
style?: 'default' | 'success' | 'danger'| 'warning' | 'info' | 'primary' | 'text' | null;
className?: string | null;
scope?: string | null;
title?: string | null;
titleTranslation?: string | null;
label?: string | null;
labelTranslation?: string | null;
link?: string | null;
iconClass?: string | null;
text?: string | null;
disabled?: boolean;
hidden?: boolean;
html?: string | null;
iconHtml?: string | null;
data?: Record<string, unknown>;
onClick?: (event: MouseEvent) => void;
}
interface DropdownItemOptions {
name: string | null;
action?: string | null;
className?: string | null;
scope?: string | null;
title?: string | null;
titleTranslation?: string | null;
label?: string | null;
labelTranslation?: string | null;
link?: string | null;
iconClass?: string | null;
text?: string | null;
disabled?: boolean;
hidden?: boolean;
html?: string | null;
iconHtml?: string | null;
data?: Record<string, unknown>;
onClick?: (event: MouseEvent) => void;
}
/**
* @internal
* @experimental
* @since 10.0.0
*/
export class ButtonComponent {
@inject(Language)
private language: Language
constructor(private options: ButtonOptions) {}
node(): VNode {
const classes: any = {
'btn': true,
'disabled': this.options.disabled === true,
'hidden': this.options.hidden === true,
'action': this.options.action != null,
};
const style = this.options.style ?? 'default';
classes['btn-' + style] = true;
if (this.options.className) {
this.options.className.split(' ').forEach(it => classes[it.trim()] = true);
}
const tag: 'button' | 'a' = this.options.link ? 'a' : 'button';
const attrs: Record<string, any> = {};
if (this.options.link) {
attrs.href = this.options.link;
} else {
attrs.type = 'button';
}
if (this.options.disabled) {
attrs.disabled = 'disabled';
}
if (this.options.title) {
attrs.title = this.options.title;
}
if (!this.options.title && this.options.titleTranslation) {
attrs.title = this.language.translatePath(this.options.titleTranslation);
}
const {content, props} = prepareItemContent(this.options, this.language);
const on: any = {};
if (this.options.onClick) {
on.click = (event: MouseEvent) => this.options.onClick!(event)
}
return h(tag, {
key: this.options.name ?? undefined,
class: classes,
attrs: attrs,
dataset: {
...this.options.data,
name: (this.options.name ?? undefined)!,
action: (this.options.action ?? undefined)!,
},
props,
on: on,
}, content);
}
}
/**
* @internal
* @experimental
* @since 10.0.0
*/
export class DropdownItemComponent {
@inject(Language)
private language: Language
constructor(private options: DropdownItemOptions) {}
node(): VNode {
const classes: any = {
'disabled': this.options.disabled === true,
'hidden': this.options.hidden === true,
'action': this.options.action != null,
};
if (this.options.className) {
this.options.className.split(' ').forEach(it => classes[it.trim()] = true);
}
const attrs: Record<string, any> = {
tabindex: 0,
};
if (this.options.link) {
attrs.href = this.options.link;
}
if (this.options.disabled) {
attrs.disabled = 'disabled';
}
if (this.options.title) {
attrs.title = this.options.title;
}
if (!this.options.title && this.options.titleTranslation) {
attrs.title = this.language.translatePath(this.options.titleTranslation);
}
const {content, props} = prepareItemContent(this.options, this.language, true);
if (!this.options.link) {
attrs.role = 'button';
}
const on: any = {};
if (this.options.onClick) {
on.click = (event: MouseEvent) => this.options.onClick!(event)
}
const a = h('a', {
class: classes,
attrs: attrs,
dataset: {
...this.options.data,
name: (this.options.name ?? undefined)!,
action: (this.options.action ?? undefined)!,
},
props: props,
on: on,
}, content);
return h('li', {
key: this.options.name ?? undefined,
class: {
'disabled': this.options.disabled === true,
'hidden': this.options.hidden === true,
},
}, a);
}
}
function prepareItemContent(
options: ButtonOptions | DropdownItemOptions,
language: Language,
isDropdownItem: boolean = false,
): {props: Record<string, unknown>, content: any} {
let content = null;
const props: Record<string, unknown> = {};
if (options.html) {
props.innerHTML = options.html;
if (options.iconHtml) {
props.innerHTML = options.iconHtml + props.innerHTML;
}
} else {
const label = options.label;
let text = options.text ??
(
options.labelTranslation ?
language.translatePath(options.labelTranslation) :
language.translate(label ?? '', 'labels', options.scope)
);
let icon = null;
if (options.iconHtml) {
const div = document.createElement('div');
div.innerHTML = options.iconHtml;
icon = div.firstElementChild ? toVNode(div.firstElementChild) : null;
} else if (options.iconClass) {
icon = h('span', {props: {className: options.iconClass + ' item-icon'}});
}
if (isDropdownItem) {
text = h('span', {props: {className: 'item-text'}}, text);
} else {
if (icon && text) {
text = h('span', text);
}
}
content = icon ?
[
icon,
text,
] :
text;
}
return {content, props};
}

View File

@@ -0,0 +1,107 @@
/************************************************************************
* 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 type Model from 'model';
import type {AdvancedFilter} from 'search-manager';
export interface SelectRelatedFilters {
/**
* Advanced filters. A field name as a key.
*/
advanced?: Record<string, AdvancedFilter>;
/**
* Bool filters.
*/
bool?: string[];
/**
* A primary filter.
*/
primary?: string;
/**
* A field to order by.
*/
orderBy?: string;
/**
* An order direction.
*/
order?: 'asc' | 'desc';
}
/**
* Select related records. Prepares filters for selecting records to relate.
*/
export interface SelectRelatedHandler<M extends Model = Model> {
/**
* Filters.
*
* @param model A parent model.
* @return {Promise<SelectRelatedFilters>} Filters.
*/
getFilters(model: M): Promise<SelectRelatedFilters>;
}
/**
* Prepares attributes for a related record that is being created.
*
* @param model A parent model.
*/
export interface CreateRelatedHandler<M extends Model = Model> {
/**
* Get attributes for a new record.
*
* @param model A model.
* @param link A link name.
*/
getAttributes(model: M, link: string): Promise<Record<string, unknown>>;
}
/**
* Prepares attributes to set to the model when selecting and clearing a related record.
*.
* @template M A related model type.
* @template P A parent model type.
*/
export interface SelectFieldHandler<M extends Model = Model> {
/**
* Get attributes to set to the model after selecting the related model.
*
* @param model A related model.
* @return {Promise<Record<string, unknown>>} Attributes.
*/
getAttributes: (model: M) => Promise<Record<string, unknown>>;
/**
* Get attributes to set to the model after clearing a related model. Usually, it's null values.
*
* @return {Promise<Record<string, unknown>>} Attributes.
*/
getClearAttributes: () => Promise<Record<string, unknown>>;
}

789
client/src/controller.js Normal file
View File

@@ -0,0 +1,789 @@
/************************************************************************
* 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.
************************************************************************/
/** @module controller */
import Exceptions from 'exceptions';
import {Events, View as BullView} from 'bullbone';
import {inject} from 'di';
import ModalBarProvider from 'helpers/site/modal-bar-provider';
/**
* @callback module:controller~viewCallback
* @param {import('view')} view A view.
*/
/**
* @callback module:controller~masterViewCallback
* @param {module:views/site/master} view A master view.
*/
/**
* A controller. To be extended.
*
* @mixes Events
*/
class Controller {
/**
* @internal
* @param {Object.<string, *>} params
* @param {Object} injections
*/
constructor(params, injections) {
this.params = params || {};
/** @type {module:controllers/base} */
this.baseController = injections.baseController;
this.viewFactory = injections.viewFactory;
/** @type {import('model-factory').default} */
this.modelFactory = injections.modelFactory;
/** @type {import('collection-factory').default} */
this.collectionFactory = injections.collectionFactory;
this._settings = injections.settings || null;
this._user = injections.user || null;
this._preferences = injections.preferences || null;
this._acl = injections.acl || null;
this._cache = injections.cache || null;
this._router = injections.router || null;
this._storage = injections.storage || null;
this._metadata = injections.metadata || null;
this._dateTime = injections.dateTime || null;
this._broadcastChannel = injections.broadcastChannel || null;
this.setMasterRendered(false);
}
/**
* A default action.
*
* @type {string}
*/
defaultAction = 'index'
/**
* A name.
*
* @type {string|null}
*/
name = null
/**
* Params.
*
* @type {Object}
* @private
*/
params = null
/**
* A view factory.
*
* @type {import('bullbone').Factory}
* @protected
*/
viewFactory = null
/**
* A model factory.
*
* @type {module:model-factory}
* @protected
*/
modelFactory = null
/**
* A body view.
*
* @public
* @type {string|null}
*/
masterView = null
/**
* @private
* @type {ModalBarProvider}
*/
@inject(ModalBarProvider)
modalBarProvider
/**
* Set the router.
*
* @internal
* @param {import(router).default} router
*/
setRouter(router) {
this._router = router;
this.trigger('router-set', router);
}
/**
* @protected
* @returns {module:models/settings}
*/
getConfig() {
return this._settings;
}
/**
* @protected
* @returns {module:models/user}
*/
getUser() {
return this._user;
}
/**
* @protected
* @returns {module:models/preferences}
*/
getPreferences() {
return this._preferences;
}
/**
* @protected
* @returns {module:acl-manager}
*/
getAcl() {
return this._acl;
}
/**
* @protected
* @returns {module:cache}
*/
getCache() {
return this._cache;
}
/**
* @protected
* @returns {import('router').default}
*/
getRouter() {
return this._router;
}
/**
* @protected
* @returns {module:storage}
*/
getStorage() {
return this._storage;
}
/**
* @protected
* @returns {module:metadata}
*/
getMetadata() {
return this._metadata;
}
/**
* @protected
* @returns {module:date-time}
*/
getDateTime() {
return this._dateTime;
}
/**
* Get a parameter of all controllers.
*
* @param {string} key A key.
* @return {*} Null if a key doesn't exist.
*/
get(key) {
if (key in this.params) {
return this.params[key];
}
return null;
}
/**
* Set a parameter for all controllers.
*
* @param {string} key A name of a view.
* @param {*} value
*/
set(key, value) {
this.params[key] = value;
}
/**
* Unset a parameter.
*
* @param {string} key A key.
*/
unset(key) {
delete this.params[key];
}
/**
* Has a parameter.
*
* @param {string} key A key.
* @returns {boolean}
*/
has(key) {
return key in this.params;
}
/**
* @param {string} key
* @param {string} [name]
* @return {string}
* @private
*/
_composeScrollKey(key, name) {
name = name || this.name;
return `scrollTop-${name}-${key}`;
}
/**
* @param {string} key
* @return {string}
* @private
*/
_composeMainViewKey(key) {
return `mainView-${this.name}-${key}`;
}
/**
* Get a stored main view.
*
* @param {string} key A key.
* @returns {import('view').default|null}
*/
getStoredMainView(key) {
return this.get(this._composeMainViewKey(key));
}
/**
* Has a stored main view.
* @param {string} key
* @returns {boolean}
*/
hasStoredMainView(key) {
return this.has(this._composeMainViewKey(key));
}
/**
* Clear a stored main view.
* @param {string} key
*/
clearStoredMainView(key) {
const view = this.getStoredMainView(key);
if (view) {
view.remove(true);
}
this.unset(this._composeScrollKey(key));
this.unset(this._composeMainViewKey(key));
}
/**
* Store a main view.
*
* @param {string} key A key.
* @param {import('view').default} view A view.
*/
storeMainView(key, view) {
this.set(this._composeMainViewKey(key), view);
this.listenTo(view, 'remove', (o) => {
o = o || {};
if (o.ignoreCleaning) {
return;
}
this.stopListening(view, 'remove');
this.clearStoredMainView(key);
});
}
/**
* Check access to an action.
*
* @param {string} action An action.
* @returns {boolean}
*/
checkAccess(action) {
return true;
}
/**
* Process access check to the controller.
*/
handleAccessGlobal() {
if (!this.checkAccessGlobal()) {
throw new Exceptions.AccessDenied("Denied access to '" + this.name + "'");
}
}
/**
* Check access to the controller.
*
* @returns {boolean}
*/
checkAccessGlobal() {
return true;
}
/**
* Check access to an action. Throwing an exception.
*
* @param {string} action An action.
*/
handleCheckAccess(action) {
if (this.checkAccess(action)) {
return;
}
const msg = action ?
"Denied access to action '" + this.name + "#" + action + "'" :
"Denied access to scope '" + this.name + "'";
throw new Exceptions.AccessDenied(msg);
}
/**
* Process an action.
*
* @param {string} action
* @param {Object} options
*/
doAction(action, options) {
this.handleAccessGlobal();
action = action || this.defaultAction;
const method = 'action' + Espo.Utils.upperCaseFirst(action);
if (!(method in this)) {
throw new Exceptions.NotFound("Action '" + this.name + "#" + action + "' is not found");
}
const preMethod = 'before' + Espo.Utils.upperCaseFirst(action);
const postMethod = 'after' + Espo.Utils.upperCaseFirst(action);
if (preMethod in this) {
this[preMethod].call(this, options || {});
}
this[method].call(this, options || {});
if (postMethod in this) {
this[postMethod].call(this, options || {});
}
}
/**
* Serve a master view. Render if not already rendered.
*
* @param {module:controller~masterViewCallback} callback A callback with a created master view.
* @private
*/
master(callback) {
const entireView = this.getEntireView();
if (entireView) {
entireView.remove();
this.setEntireView(null);
}
const masterView = this.getMasterView();
if (masterView) {
callback.call(this, masterView);
return;
}
const viewName = this.masterView || 'views/site/master';
this.viewFactory.create(viewName, {fullSelector: 'body'},
async /** import('views/site/master').default */masterView => {
this.setMasterView(masterView);
if (this.isMasterRendered()) {
callback.call(this, masterView);
return;
}
this.modalBarProvider.set(masterView.collapsedModalBarView || null)
await masterView.render();
this.setMasterRendered(true);
callback.call(this, masterView);
});
}
/**
* @private
* @return {import('view').default|null}
*/
getEntireView() {
return this.get('entire');
}
/**
* @private
* @param {import('view').default|null} view
*/
setEntireView(view) {
this.set('entire', view);
}
/**
* @private
* @return {import('views/site/master').default|null}
*/
getMasterView() {
return this.get('master');
}
/**
* @private
* @param {import('views/site/master').default|null} view
*/
setMasterView(view) {
if (!view) {
this.modalBarProvider.set(null);
}
this.set('master', view);
}
/**
* @private
* @param {boolean} value
*/
setMasterRendered(value) {
this.set('masterRendered', value);
}
/**
* @private
* @return {boolean}
*/
isMasterRendered() {
return !!this.get('masterRendered');
}
/**
* @param {import('views/site/master').default} masterView
* @private
*/
_unchainMainView(masterView) {
if (
!masterView.currentViewKey /*||
!this.hasStoredMainView(masterView.currentViewKey)*/
) {
return;
}
const currentMainView = masterView.getMainView();
if (!currentMainView) {
return;
}
currentMainView.propagateEvent('remove', {ignoreCleaning: true});
masterView.unchainView('main');
}
/**
* @typedef {Object} module:controller~mainParams
* @property {boolean} [useStored] Use a stored view if available.
* @property {string} [key] A stored view key.
*/
/**
* Create a main view in the master container and render it.
*
* @param {string|import('view').default} [view] A view name or view instance.
* @param {Object.<string, *>} [options] Options for a view.
* @param {module:controller~viewCallback} [callback] A callback with a created view.
* @param {module:controller~mainParams} [params] Parameters.
*/
main(view, options, callback, params = {}) {
const dto = {
isCanceled: false,
key: params.key,
useStored: params.useStored,
callback: callback,
};
const selector = '#main';
const useStored = params.useStored || false;
const key = params.key;
this.listenToOnce(this.baseController, 'action', () => dto.isCanceled = true);
const mainView = view && typeof view === 'object' ?
view : undefined;
const viewName = !mainView ?
(view || 'views/base') : undefined;
this.master(async masterView => {
if (dto.isCanceled) {
return;
}
options = options || {};
options.fullSelector = selector;
if (useStored && this.hasStoredMainView(key)) {
const mainView = this.getStoredMainView(key);
let isActual = true;
if (
mainView &&
('isActualForReuse' in mainView) &&
typeof mainView.isActualForReuse === 'function'
) {
isActual = mainView.isActualForReuse();
}
const lastUrl = (mainView && 'lastUrl' in mainView) ? mainView.lastUrl : null;
if (
isActual &&
(!lastUrl || lastUrl === this.getRouter().getCurrentUrl())
) {
this._processMain(mainView, masterView, dto);
if (
'setupReuse' in mainView &&
typeof mainView.setupReuse === 'function'
) {
mainView.setupReuse(options.params || {});
}
return;
}
this.clearStoredMainView(key);
}
if (mainView) {
this._unchainMainView(masterView);
await masterView.assignView('main', mainView, selector)
dto.isSet = true;
this._processMain(view, masterView, dto);
return;
}
this.viewFactory.create(viewName, options, view => {
this._processMain(view, masterView, dto);
});
});
}
/**
* @param {import('view').default} mainView
* @param {import('views/site/master').default} masterView
* @param {{
* isCanceled: boolean,
* key?: string,
* useStored?: boolean,
* callback?: module:controller~viewCallback,
* isSet?: boolean,
* }} dto Data.
* @private
*/
_processMain(mainView, masterView, dto) {
if (dto.isCanceled) {
return;
}
const key = dto.key;
if (key) {
this.storeMainView(key, mainView);
}
const onAction = () => {
mainView.cancelRender();
dto.isCanceled = true;
};
mainView.listenToOnce(this.baseController, 'action', onAction);
if (masterView.currentViewKey) {
const scrollKey = this._composeScrollKey(masterView.currentViewKey, masterView.currentName);
this.set(scrollKey, window.scrollY);
if (!dto.isSet) {
this._unchainMainView(masterView);
}
}
masterView.currentViewKey = key;
masterView.currentName = this.name;
if (!dto.isSet) {
masterView.setView('main', mainView);
}
const afterRender = () => {
setTimeout(() => mainView.stopListening(this.baseController, 'action', onAction), 500);
mainView.updatePageTitle();
const scrollKey = this._composeScrollKey(key);
if (dto.useStored && this.has(scrollKey)) {
window.scrollTo({top: this.get(scrollKey)})
return;
}
window.scrollTo({top: 0})
};
if (dto.callback) {
this.listenToOnce(mainView, 'after:render', afterRender);
dto.callback.call(this, mainView);
return;
}
mainView.render()
.then(afterRender);
}
/**
* Show a loading notify-message.
*/
showLoadingNotification() {
const master = this.getMasterView();
if (!master) {
return;
}
master.showLoadingNotification();
}
/**
* Hide a loading notify-message.
*/
hideLoadingNotification() {
const master = this.getMasterView();
if (!master) {
return;
}
master.hideLoadingNotification();
}
/**
* Create a view in the BODY element. Use for rendering separate pages without the default navbar and footer.
* If a callback is not passed, the view will be automatically rendered.
*
* @param {string|import('view').default} view A view name or view instance.
* @param {Object.<string, *>} [options] Options for a view.
* @param {module:controller~viewCallback} [callback] A callback with a created view.
*/
entire(view, options, callback) {
const masterView = this.getMasterView();
if (masterView) {
masterView.remove();
}
this.setMasterView(null);
this.setMasterRendered(false);
if (typeof view === 'object') {
view.setSelector('body');
view.setElement('body');
this.viewFactory.prepare(view, () => {
if (!callback) {
view.render();
return;
}
callback(view);
});
return;
}
options = options || {};
options.fullSelector = 'body';
this.viewFactory.create(view, options, view => {
this.setEntireView(view);
if (!callback) {
view.render();
return;
}
callback(view);
});
}
}
Object.assign(Controller.prototype, Events);
/** For backward compatibility. */
Controller.extend = BullView.extend;
export default Controller;

View File

@@ -0,0 +1,43 @@
/************************************************************************
* 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 Controller from 'controller';
class AboutController extends Controller {
defaultAction = 'about'
// noinspection JSUnusedGlobalSymbols
actionAbout() {
this.main('About', {}, view => {
view.render();
});
}
}
export default AboutController;

View File

@@ -0,0 +1,64 @@
/************************************************************************
* 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 Controller from 'controller';
class AddressMapController extends Controller {
defaultAction = 'index'
// noinspection JSUnusedGlobalSymbols
actionIndex() {
this.error404();
}
// noinspection JSUnusedGlobalSymbols
/**
* @param {Object} o
*/
actionView(o) {
this.modelFactory
.create(o.entityType)
.then(model => {
model.id = o.id;
model.fetch()
.then(() => {
const viewName = this.getMetadata().get(['AddressMap', 'view']) ||
'views/address-map/view';
this.main(viewName, {
model: model,
field: o.field,
});
});
});
}
}
export default AddressMapController;

View File

@@ -0,0 +1,579 @@
/************************************************************************
* 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 Controller from 'controller';
import SearchManager from 'search-manager';
import AdminIndexView from 'views/admin/index';
import {inject} from 'di';
import Language from 'language';
import EditView from 'views/edit';
import AdminPipelinesListForEntityType from 'views/admin/pipelines/list-for-entity-type';
class AdminController extends Controller {
/**
* @private
* @type {Language}
*/
@inject(Language)
language
checkAccessGlobal() {
if (this.getUser().isAdmin()) {
return true;
}
return false;
}
// noinspection JSUnusedGlobalSymbols
async actionPage(options) {
const page = options.page;
if (options.options) {
options = {
...Espo.Utils.parseUrlOptionsParam(options.options),
...options,
};
delete options.options;
}
if (!page) {
throw new Error();
}
const methodName = 'action' + Espo.Utils.upperCaseFirst(page);
if (this[methodName]) {
this[methodName](options);
return;
}
const defs = this.getPageDefs(page);
if (!defs) {
throw new Espo.Exceptions.NotFound();
}
if (defs.view && !defs.recordView) {
this.main(defs.view, options);
return;
}
if (!defs.recordView) {
throw new Espo.Exceptions.NotFound();
}
const model = this.getSettingsModel();
await model.fetch();
model.id = '1';
const view = defs.view ?? 'views/settings/edit';
const ViewClass = await Espo.loader.requirePromise(view);
if (!EditView.isPrototypeOf(ViewClass)) {
throw new Error("View should inherit views/edit.");
}
let label = defs.label;
if (defs.labelTranslation) {
label = this.language.translatePath(defs.labelTranslation);
}
const editView = new ViewClass({
model: model,
headerTemplate: 'admin/settings/headers/page',
recordView: defs.recordView,
page: page,
label: label,
optionsToPass: [
'page',
'label',
],
});
this.main(editView);
}
// noinspection JSUnusedGlobalSymbols
actionIndex(options) {
let isReturn = options.isReturn;
const key = 'index';
if (this.getRouter().backProcessed) {
isReturn = true;
}
if (!isReturn && this.getStoredMainView(key)) {
this.clearStoredMainView(key);
}
const view = new AdminIndexView();
this.main(view, null, view => {
view.render();
this.listenTo(view, 'clear-cache', this.clearCache);
this.listenTo(view, 'rebuild', this.rebuild);
}, {
useStored: isReturn,
key: key,
});
}
// noinspection JSUnusedGlobalSymbols
actionUsers() {
this.getRouter().dispatch('User', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionPortalUsers() {
this.getRouter().dispatch('PortalUser', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionApiUsers() {
this.getRouter().dispatch('ApiUser', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionTeams() {
this.getRouter().dispatch('Team', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionRoles() {
this.getRouter().dispatch('Role', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionPortalRoles() {
this.getRouter().dispatch('PortalRole', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionPortals() {
this.getRouter().dispatch('Portal', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionLeadCapture() {
this.getRouter().dispatch('LeadCapture', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionEmailFilters() {
this.getRouter().dispatch('EmailFilter', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionGroupEmailFolders() {
this.getRouter().dispatch('GroupEmailFolder', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionEmailTemplates() {
this.getRouter().dispatch('EmailTemplate', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionPdfTemplates() {
this.getRouter().dispatch('Template', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionDashboardTemplates() {
this.getRouter().dispatch('DashboardTemplate', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionWebhooks() {
this.getRouter().dispatch('Webhook', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionLayoutSets() {
this.getRouter().dispatch('LayoutSet', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionWorkingTimeCalendar() {
this.getRouter().dispatch('WorkingTimeCalendar', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionAttachments() {
this.getRouter().dispatch('Attachment', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionAuthenticationProviders() {
this.getRouter().dispatch('AuthenticationProvider', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionOAuthClients() {
this.getRouter().dispatch('OAuthClient', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionAddressCountries() {
this.getRouter().dispatch('AddressCountry', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionEmailAddresses() {
this.getRouter().dispatch('EmailAddress', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionPhoneNumbers() {
this.getRouter().dispatch('PhoneNumber', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionPersonalEmailAccounts() {
this.getRouter().dispatch('EmailAccount', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionGroupEmailAccounts() {
this.getRouter().dispatch('InboundEmail', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionActionHistory() {
this.getRouter().dispatch('ActionHistoryRecord', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionImport() {
this.getRouter().dispatch('Import', 'index', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionLayouts(options) {
const scope = options.scope || null;
const type = options.type || null;
const em = options.em || false;
this.main('views/admin/layouts/index', {scope: scope, type: type, em: em});
}
// noinspection JSUnusedGlobalSymbols
actionLabelManager(options) {
const scope = options.scope || null;
const language = options.language || null;
this.main('views/admin/label-manager/index', {scope: scope, language: language});
}
// noinspection JSUnusedGlobalSymbols
actionTemplateManager(options) {
const name = options.name || null;
this.main('views/admin/template-manager/index', {name: name});
}
// noinspection JSUnusedGlobalSymbols
actionFieldManager(options) {
const scope = options.scope || null;
const field = options.field || null;
this.main('views/admin/field-manager/index', {scope: scope, field: field});
}
// noinspection JSUnusedGlobalSymbols
/**
* @param {Record} options
*/
actionEntityManager(options) {
const scope = options.scope || null;
if (scope && options.edit) {
this.main('views/admin/entity-manager/edit', {scope: scope});
return;
}
if (options.create) {
this.main('views/admin/entity-manager/edit');
return;
}
if (scope && options.formula) {
this.main('views/admin/entity-manager/formula', {scope: scope, type: options.type});
return;
}
if (scope) {
this.main('views/admin/entity-manager/scope', {scope: scope});
return;
}
this.main('views/admin/entity-manager/index');
}
// noinspection JSUnusedGlobalSymbols
actionLinkManager(options) {
const scope = options.scope || null;
this.main('views/admin/link-manager/index', {scope: scope});
}
// noinspection JSUnusedGlobalSymbols
actionSystemRequirements() {
this.main('views/admin/system-requirements/index');
}
/**
* @returns {module:models/settings}
*/
getSettingsModel() {
const model = this.getConfig().clone();
model.defs = this.getConfig().defs;
this.listenTo(model, 'after:save', () => {
this.getConfig().load();
this._broadcastChannel.postMessage('update:config');
});
// noinspection JSValidateTypes
return model;
}
// noinspection JSUnusedGlobalSymbols
actionAuthTokens() {
this.collectionFactory.create('AuthToken', collection => {
const searchManager = new SearchManager(collection, {storageKey: 'list'});
searchManager.loadStored();
collection.where = searchManager.getWhere();
collection.maxSize = this.getConfig().get('recordsPerPage') || collection.maxSize;
this.main('views/admin/auth-token/list', {
scope: 'AuthToken',
collection: collection,
searchManager: searchManager
});
});
}
// noinspection JSUnusedGlobalSymbols
/**
*
* @param {Record} options
*/
async actionPipelines(options = {}) {
if (options?.scope) {
const collection = await this.collectionFactory.create('Pipeline');
collection.maxSize = this.getConfig().get('recordsPerPage');
collection.where = [{
type: 'equals',
attribute: 'entityType',
value: options.scope,
}];
const view = new AdminPipelinesListForEntityType({
scope: 'Pipeline',
collection,
targetEntityType: options.scope,
});
this.main(view);
return;
}
this.main('views/admin/pipelines/main');
}
// noinspection JSUnusedGlobalSymbols
actionAuthLog() {
this.collectionFactory.create('AuthLogRecord', collection => {
const searchManager = new SearchManager(collection, {storageKey: 'list'});
searchManager.loadStored();
collection.where = searchManager.getWhere();
collection.maxSize = this.getConfig().get('recordsPerPage') || collection.maxSize;
this.main('views/admin/auth-log-record/list', {
scope: 'AuthLogRecord',
collection: collection,
searchManager: searchManager
});
});
}
// noinspection JSUnusedGlobalSymbols
actionAppSecrets() {
this.getRouter().dispatch('AppSecret', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionOAuthProviders() {
this.getRouter().dispatch('OAuthProvider', 'list', {fromAdmin: true});
}
// noinspection JSUnusedGlobalSymbols
actionJobs() {
this.collectionFactory.create('Job', collection => {
const searchManager = new SearchManager(collection, {storageKey: 'list'});
searchManager.loadStored();
collection.where = searchManager.getWhere();
collection.maxSize = this.getConfig().get('recordsPerPage') || collection.maxSize;
this.main('views/admin/job/list', {
scope: 'Job',
collection: collection,
searchManager: searchManager,
});
});
}
// noinspection JSUnusedGlobalSymbols
actionAppLog() {
this.collectionFactory.create('AppLogRecord', collection => {
const searchManager = new SearchManager(collection, {storageKey: 'list'});
searchManager.loadStored();
collection.where = searchManager.getWhere();
collection.maxSize = this.getConfig().get('recordsPerPage') || collection.maxSize;
this.main('views/list', {
scope: 'AppLogRecord',
collection: collection,
searchManager: searchManager
});
});
}
// noinspection JSUnusedGlobalSymbols
actionIntegrations(options) {
const integration = options.name || null;
this.main('views/admin/integrations/index', {integration: integration});
}
// noinspection JSUnusedGlobalSymbols
actionExtensions() {
this.main('views/admin/extensions/index');
}
rebuild() {
if (this.rebuildRunning) {
return;
}
this.rebuildRunning = true;
Espo.Ui.notify(this.language.translate('pleaseWait', 'messages'));
Espo.Ajax
.postRequest('Admin/rebuild')
.then(() => {
const msg = this.language.translate('Rebuild has been done', 'labels', 'Admin');
Espo.Ui.success(msg);
this.rebuildRunning = false;
})
.catch(() => {
this.rebuildRunning = false;
});
}
clearCache() {
if (this.clearCacheRunning) {
return;
}
this.clearCacheRunning = true;
Espo.Ui.notify(this.language.translate('pleaseWait', 'messages'));
Espo.Ajax.postRequest('Admin/clearCache')
.then(() => {
const msg = this.language.translate('Cache has been cleared', 'labels', 'Admin');
Espo.Ui.success(msg);
this.clearCacheRunning = false;
})
.catch(() => {
this.clearCacheRunning = false;
});
}
/**
* @returns {Object|null}
*/
getPageDefs(page) {
const panelsDefs = this.getMetadata().get(['app', 'adminPanel']) || {};
let resultDefs = null;
for (const panelKey in panelsDefs) {
const itemList = panelsDefs[panelKey].itemList || [];
for (const defs of itemList) {
if (defs.url === '#Admin/' + page) {
resultDefs = defs;
break;
}
}
if (resultDefs) {
break;
}
}
return resultDefs;
}
}
export default AdminController;

View File

@@ -0,0 +1,75 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class ApiUserController extends RecordController {
entityType ='User'
getCollection(usePreviouslyFetched) {
return super.getCollection()
.then(collection => {
collection.data.userType = 'api';
return collection;
});
}
/**
* @protected
* @param {Object} options
* @param {module:models/user} model
* @param {string} view
*/
createViewView(options, model, view) {
if (!model.isApi()) {
if (model.isPortal()) {
this.getRouter().dispatch('PortalUser', 'view', {id: model.id, model: model});
return;
}
this.getRouter().dispatch('User', 'view', {id: model.id, model: model});
return;
}
super.createViewView(options, model, view);
}
actionCreate(options) {
options = options || {};
options.attributes = options.attributes || {};
options.attributes.type = 'api';
super.actionCreate(options);
}
}
export default ApiUserController;

View File

@@ -0,0 +1,235 @@
/************************************************************************
* 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.
************************************************************************/
/** @module controllers/base */
import Controller from 'controller';
import BaseView from 'views/base';
/**
* A base controller.
*/
class BaseController extends Controller {
constructor(params, injections) {
super(params, injections);
this.on('logout', () => this._clearAllStoredMainViews());
}
/**
* @private
*/
_clearAllStoredMainViews() {
for (const name in this.params) {
if (!name.startsWith('mainView-')) {
continue;
}
const [, scope, key] = name.split('-', 3);
const actualKey = `mainView-${scope}-${key}`;
const view =
/** @type {import('view').default} */
this.get(actualKey);
if (view) {
view.remove(true);
}
this.unset(actualKey);
}
}
/**
* Clear a stored main view.
*
* @param {string} scope
*/
clearScopeStoredMainView(scope) {
for (const key in this.params) {
if (!key.startsWith(`mainView-${scope}-`)) {
continue;
}
const view =
/** @type {import('view').default} */
this.get(key);
if (view) {
view.remove(true);
}
this.unset(key);
}
}
/**
* Log in.
*
* @param {{
* anotherUser?: string,
* username?: string,
* }} [options]
*/
login(options) {
const viewName = this.getConfig().get('loginView') || 'views/login';
const anotherUser = (options || {}).anotherUser;
const prefilledUsername = (options || {}).username;
const viewOptions = {
anotherUser: anotherUser,
prefilledUsername: prefilledUsername,
};
this.entire(viewName, viewOptions, loginView => {
loginView.render();
loginView.on('login', (userName, data) => {
this.trigger('login', this.normalizeLoginData(userName, data));
});
loginView.once('redirect', (viewName, headers, userName, password, data) => {
loginView.remove();
this.entire(viewName, {
loginData: data,
userName: userName,
password: password,
anotherUser: anotherUser,
headers: headers,
}, secondStepView => {
secondStepView.render();
secondStepView.once('login', (userName, data) => {
this.trigger('login', this.normalizeLoginData(userName, data));
});
secondStepView.once('back', () => {
secondStepView.remove();
this.login();
});
});
});
});
}
/** @private */
normalizeLoginData(userName, data) {
return {
auth: {
userName: userName,
token: data.token,
anotherUser: data.anotherUser,
},
user: data.user,
preferences: data.preferences,
acl: data.acl,
settings: data.settings,
appParams: data.appParams,
language: data.language,
};
}
/**
* Log out.
*/
logout() {
const title = this.getConfig().get('applicationName') || 'EspoCRM';
$('head title').text(title);
this.trigger('logout');
}
/**
* Clear cache.
*/
clearCache() {
this.entire('views/clear-cache', {
cache: this.getCache(),
}, view => {
view.render();
});
}
// noinspection JSUnusedGlobalSymbols
actionLogin() {
this.login();
}
// noinspection JSUnusedGlobalSymbols
actionLogout() {
this.logout();
}
// noinspection JSUnusedGlobalSymbols
actionLogoutWait() {
this.entire('views/base', {template: 'logout-wait'}, view => {
view.render()
.then(() => Espo.Ui.notifyWait())
});
}
// noinspection JSUnusedGlobalSymbols
actionClearCache() {
this.clearCache();
}
/**
* Error Not Found.
*/
error404() {
const view = new BaseView({template: 'errors/404'});
this.entire(view);
}
/**
* Error Forbidden.
*/
error403() {
const view = new BaseView({template: 'errors/403'});
this.entire(view);
}
// noinspection JSUnusedGlobalSymbols
actionError404() {
this.error404();
}
// noinspection JSUnusedGlobalSymbols
actionError403() {
this.error403();
}
}
export default BaseController;

View File

@@ -0,0 +1,45 @@
/************************************************************************
* 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 Controller from 'controller';
class DashboardController extends Controller {
defaultAction = 'index'
// noinspection JSUnusedGlobalSymbols
actionIndex() {
this.main('views/dashboard', {
displayTitle: true,
}, view => {
view.render();
});
}
}
export default DashboardController;

View File

@@ -0,0 +1,47 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class EmailFilterController extends RecordController {
actionCreate(options) {
const attributes = {...options.attributes};
if (options.from) {
attributes.from = options.from;
attributes.name = options.from;
options.attributes = attributes;
}
super.actionCreate(options);
}
}
export default EmailFilterController;

View File

@@ -0,0 +1,47 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class EmailController extends RecordController {
prepareModelView(model, options) {
super.prepareModelView(model, options);
this.listenToOnce(model, 'after:send', () => {
const key = 'list';
const stored = this.getStoredMainView(key);
if (stored) {
this.clearStoredMainView(key);
}
});
}
}
export default EmailController;

View File

@@ -0,0 +1,67 @@
/************************************************************************
* 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 Controller from 'controller';
class ExternalAccountController extends Controller {
defaultAction = 'list'
actionList() {
this.collectionFactory.create('ExternalAccount', collection => {
collection.once('sync', () => {
this.main('ExternalAccount.Index', {
collection: collection,
});
});
collection.fetch();
});
}
// noinspection JSUnusedGlobalSymbols
/**
* @param {{id: string}} options
*/
actionEdit(options) {
const id = options.id;
this.collectionFactory.create('ExternalAccount', collection => {
collection.once('sync', () => {
this.main('ExternalAccount.Index', {
collection: collection,
id: id,
});
});
collection.fetch();
});
}
}
export default ExternalAccountController;

View File

@@ -0,0 +1,52 @@
/************************************************************************
* 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 Controller from 'controller';
import GlobalStreamView from 'views/global-stream';
class GlobalStreamController extends Controller {
// noinspection JSUnusedGlobalSymbols
actionIndex() {
const key = 'index';
const isReturn = this.getRouter().backProcessed;
if (!isReturn) {
this.clearStoredMainView(key);
}
const view = new GlobalStreamView();
this.main(view, undefined, undefined, {
key: key,
useStored: isReturn,
});
}
}
export default GlobalStreamController;

View File

@@ -0,0 +1,39 @@
/************************************************************************
* 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 Controller from 'controller';
class HomeController extends Controller {
// noinspection JSUnusedGlobalSymbols
actionIndex() {
this.main('views/home', null);
}
}
export default HomeController;

View File

@@ -0,0 +1,110 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class ImportController extends RecordController {
defaultAction = 'index'
storedData
checkAccessGlobal() {
if (this.getAcl().checkScope('Import')) {
return true;
}
return false;
}
checkAccess(action) {
if (this.getAcl().checkScope('Import')) {
return true;
}
return false;
}
// noinspection JSUnusedGlobalSymbols
/**
* @param {{
* step?: int|string,
* fromAdmin?: boolean,
* formData?: Object
* }} o
*/
actionIndex(o) {
o = o || {};
let step = null;
if (o.step) {
step = parseInt(step);
}
let formData = null;
let fileContents = null;
if (o.formData) {
this.storedData = undefined;
}
if (this.storedData) {
formData = this.storedData.formData;
fileContents = this.storedData.fileContents;
}
if (!formData) {
step = null;
}
formData = formData || o.formData;
this.main('views/import/index', {
step: step,
formData: formData,
fileContents: fileContents,
fromAdmin: o.fromAdmin,
}, /** module:views/import/index */ view => {
this.listenTo(view, 'change', () => {
this.storedData = {
formData: view.formData,
fileContents: view.fileContents,
};
});
this.listenTo(view, 'done', () => {
this.storedData = undefined;
});
view.render();
});
}
}
export default ImportController;

View File

@@ -0,0 +1,42 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class InboundEmailController extends RecordController {
checkAccess(action) {
if (this.getUser().isAdmin()) {
return true;
}
return false;
}
}
export default InboundEmailController;

View File

@@ -0,0 +1,41 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class LastViewedController extends RecordController {
entityType = 'ActionHistoryRecord'
checkAccess(action) {
return this.getAcl().check(this.entityType, action);
}
}
// noinspection JSUnusedGlobalSymbols
export default LastViewedController;

View File

@@ -0,0 +1,52 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class LayoutSetController extends RecordController {
// noinspection JSUnusedGlobalSymbols
/**
* @param {Record} options
*/
actionEditLayouts(options) {
const id = options.id;
if (!id) {
throw new Error("ID not passed.");
}
this.main('views/layout-set/layouts', {
layoutSetId: id,
scope: options.scope,
type: options.type,
});
}
}
export default LayoutSetController;

View File

@@ -0,0 +1,59 @@
/************************************************************************
* 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 Controller from 'controller';
import LeadCaptureFormView from 'views/lead-capture/form';
// noinspection JSUnusedGlobalSymbols
export default class LeadCaptureFormController extends Controller {
// noinspection JSUnusedGlobalSymbols
actionShow(data) {
this.prepareContainer();
const view = new LeadCaptureFormView({formData: data});
view.setSelector('body > .content');
this.viewFactory.prepare(view, () => view.render());
}
/**
* @private
*/
prepareContainer() {
// Prevents recaptcha removal.
const existingContainer = document.body.querySelector('.container');
existingContainer.remove();
const container = document.createElement('div');
container.classList.add('container', 'content');
document.body.prepend(container);
}
}

View File

@@ -0,0 +1,59 @@
/************************************************************************
* 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 Controller from 'controller';
class LeadCaptureOptInConfirmationController extends Controller {
// noinspection JSUnusedGlobalSymbols
actionOptInConfirmationSuccess(data) {
const viewName = this.getMetadata().get(['clientDefs', 'LeadCapture', 'optInConfirmationSuccessView']) ||
'views/lead-capture/opt-in-confirmation-success';
this.entire(viewName, {
resultData: data,
}, view => {
view.render();
});
}
// noinspection JSUnusedGlobalSymbols
actionOptInConfirmationExpired(data) {
const viewName = this.getMetadata().get(['clientDefs', 'LeadCapture', 'optInConfirmationExpiredView']) ||
'views/lead-capture/opt-in-confirmation-expired';
this.entire(viewName, {
resultData: data,
}, view => {
view.render();
});
}
}
// noinspection JSUnusedGlobalSymbols
export default LeadCaptureOptInConfirmationController;

View File

@@ -0,0 +1,60 @@
/************************************************************************
* 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 Controller from 'controller';
class LoginAsController extends Controller {
// noinspection JSUnusedGlobalSymbols
/**
* @param {Record} options
*/
actionLogin(options) {
const anotherUser = options.anotherUser;
const username = options.username;
if (!anotherUser) {
throw new Error("No anotherUser.");
}
this.baseController.login({
anotherUser: anotherUser,
username: username,
});
this.listenToOnce(this.baseController, 'login', () => {
this.baseController.once('router-set', () => {
const url = window.location.href.split('?')[0];
window.location.replace(url);
})
});
}
}
export default LoginAsController;

View File

@@ -0,0 +1,66 @@
/************************************************************************
* 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 Controller from 'controller';
class NoteController extends Controller {
// noinspection JSUnusedGlobalSymbols
/**
* @param {Record} options
*/
actionView(options) {
const id = options.id;
if (!id) {
throw new Espo.Exceptions.NotFound;
}
const viewName = this.getMetadata().get(['clientDefs', this.name, 'views', 'detail']) ||
'views/note/detail';
let model;
this.showLoadingNotification();
this.modelFactory.create('Note')
.then(m => {
model = m;
model.id = id;
return model.fetch({main: true});
})
.then(() => {
this.hideLoadingNotification();
this.main(viewName, {model: model});
});
}
}
export default NoteController;

View File

@@ -0,0 +1,43 @@
/************************************************************************
* 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 Controller from 'controller';
class NotificationController extends Controller {
defaultAction = 'index'
// noinspection JSUnusedGlobalSymbols
actionIndex() {
this.main('views/notification/list', {}, view => {
view.render();
});
}
}
export default NotificationController;

View File

@@ -0,0 +1,42 @@
/************************************************************************
* 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 Controller from 'controller';
import AuthorizationConsentView from 'views/o-auth-server/authorization-consent';
// noinspection JSUnusedGlobalSymbols
export default class OAuthAuthorizeController extends Controller {
// noinspection JSUnusedGlobalSymbols
actionShow() {
const view = new AuthorizationConsentView();
this.entire(view);
}
}

View File

@@ -0,0 +1,51 @@
/************************************************************************
* 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 Controller from 'controller';
class PasswordChangeRequestController extends Controller {
// noinspection JSUnusedGlobalSymbols
actionPasswordChange(options) {
options = options || {};
if (!options.id) {
throw new Error();
}
this.entire('views/user/password-change-request', {
requestId: options.id,
strengthParams: options.strengthParams,
notFound: options.notFound,
}, view => {
view.render();
});
}
}
export default PasswordChangeRequestController;

View File

@@ -0,0 +1,42 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class PortalRoleController extends RecordController {
checkAccess(action) {
if (this.getUser().isAdmin()) {
return true;
}
return false;
}
}
export default PortalRoleController;

View File

@@ -0,0 +1,83 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class PortalUserController extends RecordController {
entityType = 'User'
getCollection(usePreviouslyFetched) {
return super.getCollection()
.then(collection => {
collection.data.userType = 'portal';
return collection;
});
}
/**
* @protected
* @param {Object} options
* @param {module:models/user} model
* @param {string} view
*/
createViewView(options, model, view) {
if (!model.isPortal()) {
if (model.isApi()) {
this.getRouter().dispatch('ApiUser', 'view', {id: model.id, model: model});
return;
}
this.getRouter().dispatch('User', 'view', {id: model.id, model: model});
return;
}
super.createViewView(options, model, view);
}
actionCreate(options) {
options = options || {};
options.attributes = options.attributes || {};
options.attributes.type = 'portal';
super.actionCreate(options);
}
checkAccess(action) {
if (this.getAcl().getPermissionLevel('portalPermission') === 'yes') {
return true;
}
return false;
}
}
export default PortalUserController;

View File

@@ -0,0 +1,64 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
import Preferences from 'models/preferences';
class PreferencesController extends RecordController {
defaultAction = 'own'
getModel(callback, context) {
const model = new Preferences({}, {
defs: this.getMetadata().get('entityDefs.Preferences') || {},
});
model.setSettings(this.getConfig());
if (callback) {
callback.call(this, model);
}
return new Promise(resolve => {
resolve(model);
});
}
checkAccess(action) {
return true;
}
// noinspection JSUnusedGlobalSymbols
actionOwn() {
this.actionEdit({id: this.getUser().id});
}
actionList(options) {}
}
export default PreferencesController;

View File

@@ -0,0 +1,95 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
import TreeCollection from 'collections/tree';
class RecordTreeController extends RecordController {
defaultAction = 'listTree'
beforeView(options) {
super.beforeView(options);
options = options || {};
if (options.model) {
options.model.unset('childCollection');
options.model.unset('childList');
}
}
// noinspection JSUnusedGlobalSymbols
beforeListTree() {
this.handleCheckAccess('read');
}
// noinspection JSUnusedGlobalSymbols
/**
*
* @param {{
* currentId?: string,
* isReturn?: boolean,
* }} options
*/
async actionListTree(options) {
const currentId = options.currentId;
const collection = await this.getCollection();
if (!(collection instanceof TreeCollection)) {
throw new Error("Wrong collection.");
}
collection.url = `${collection.entityType}/action/listTree`;
collection.currentId = currentId ?? null;
const isReturn = options.isReturn || this.getRouter().backProcessed;
this.main(this.getViewName('listTree'), {
scope: this.name,
collection: collection,
}, undefined, {key: 'listTree', useStored: isReturn});
}
async create(options = {}) {
if (options.parentId) {
options.attributes ??= {};
options.attributes.parentId = options.parentId;
options.attributes.parentName = options.parentName;
delete options.parentId;
delete options.parentName;
}
return super.create(options);
}
}
export default RecordTreeController;

View File

@@ -0,0 +1,590 @@
/************************************************************************
* 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.
************************************************************************/
/** @module controllers/record */
import Controller from 'controller';
import DefaultsPopulator from 'helpers/model/defaults-populator';
/**
* A record controller.
* @template {module:model} TModel
*/
class RecordController extends Controller {
/** @inheritDoc */
defaultAction = 'list'
constructor(params, injections) {
super(params, injections);
/**
* @private
* @type {Object}
*/
this.collectionMap = {};
}
/** @inheritDoc */
checkAccess(action) {
if (this.getAcl().check(this.name, action)) {
return true;
}
return false;
}
/**
* Get a view name/path.
*
* @protected
* @param {'list'|'detail'|'edit'|'create'|'listRelated'|string} type A type.
* @returns {string}
*/
getViewName(type) {
return this.getMetadata().get(['clientDefs', this.name, 'views', type]) ||
'views/' + Espo.Utils.camelCaseToHyphen(type);
}
// noinspection JSUnusedGlobalSymbols
beforeList() {
this.handleCheckAccess('read');
}
/**
* @param {{
* isReturn?: boolean,
* primaryFilter?: string,
* } | Record} options
*/
actionList(options) {
const isReturn = options.isReturn || this.getRouter().backProcessed;
let key = 'list';
if (options.primaryFilter) {
key += 'Filter' + Espo.Utils.upperCaseFirst(options.primaryFilter);
}
if (!isReturn && this.getStoredMainView(key)) {
this.clearStoredMainView(key);
}
this.getCollection().then(collection => {
const mediator = {};
const abort = () => {
collection.abortLastFetch();
mediator.abort = true;
Espo.Ui.notify(false);
};
this.listenToOnce(this.baseController, 'action', abort);
this.listenToOnce(collection, 'sync', () => this.stopListening(this.baseController, 'action', abort));
const viewOptions = {
scope: this.name,
collection: collection,
params: options,
mediator: mediator,
};
const viewName = this.getViewName('list');
const params = {
useStored: isReturn,
key: key,
};
this.main(viewName, viewOptions, null, params);
});
}
beforeView() {
this.handleCheckAccess('read');
}
/**
* @protected
* @param {{
* returnUrl?: string,
* returnDispatchParams?: Record,
* } | Record} options
* @param {module:model} model
* @param {string|null} [view]
*/
createViewView(options, model, view) {
view = view || this.getViewName('detail');
this.main(view, {
scope: this.name,
model: model,
returnUrl: options.returnUrl,
returnDispatchParams: options.returnDispatchParams,
params: options,
});
}
/**
* @protected
* @param {module:model} model
* @param {Record} options
*/
prepareModelView(model, options) {}
// noinspection JSUnusedGlobalSymbols
/**
* @param {{
* model?: module:model,
* id?: string,
* isReturn?: boolean,
* isAfterCreate?: boolean,
* } | Record} options
*/
actionView(options) {
const id = options.id;
const isReturn = this.getRouter().backProcessed;
if (isReturn) {
if (this.lastViewActionOptions && this.lastViewActionOptions.id === id) {
options = Espo.Utils.clone(this.lastViewActionOptions);
if (options.model && options.model.get('deleted')) {
delete options.model;
}
}
options.isReturn = true;
}
else {
delete this.lastViewActionOptions;
}
this.lastViewActionOptions = options;
const createView = model => {
this.prepareModelView(model, options);
this.createViewView.call(this, options, model);
};
if ('model' in options) {
const model = options.model;
createView(model);
this.showLoadingNotification();
model.fetch()
.then(() => this.hideLoadingNotification())
.catch(xhr => {
if (
xhr.status === 403 &&
options.isAfterCreate
) {
this.hideLoadingNotification();
xhr.errorIsHandled = true;
model.trigger('fetch-forbidden');
}
});
this.listenToOnce(this.baseController, 'action', () => {
model.abortLastFetch();
this.hideLoadingNotification();
});
return;
}
this.getModel().then(model => {
model.id = id;
this.showLoadingNotification();
model.fetch({main: true})
.then(() => {
this.hideLoadingNotification();
if (model.get('deleted')) {
this.listenToOnce(model, 'after:restore-deleted', () => {
createView(model);
});
this.prepareModelView(model, options);
this.createViewView(options, model, 'views/deleted-detail');
return;
}
createView(model);
});
this.listenToOnce(this.baseController, 'action', () => {
model.abortLastFetch();
});
});
}
// noinspection JSUnusedGlobalSymbols
beforeCreate() {
this.handleCheckAccess('create');
}
// noinspection JSUnusedLocalSymbols
/**
* @protected
* @param {module:model} model
* @param {Record} options
*/
prepareModelCreate(model, options) {
this.listenToOnce(model, 'before:save', () => {
const key = 'list';
const stored = this.getStoredMainView(key);
if (!stored) {
return;
}
if (!('storeViewAfterCreate' in stored) || !stored.storeViewAfterCreate) {
this.clearStoredMainView(key);
}
});
this.listenToOnce(model, 'after:save', () => {
const key = 'list';
const stored = this.getStoredMainView(key);
if (!stored) {
return;
}
if (!('storeViewAfterCreate' in stored) || !stored.storeViewAfterCreate) {
return;
}
if (!('collection' in stored) || !stored.collection) {
return;
}
this.listenToOnce(stored, 'after:render', () => stored.collection.fetch());
});
}
/**
* @param {{
* options?: Record,
* relate?: import('model').SetRelateItem | import('model').SetRelateItem [],
* returnUrl?: string,
* returnDispatchParams?: Record,
* attributes?: Record,
* } | Record} [options]
*/
async create(options = {}) {
const optionsOptions = options.options || {};
const model = await this.getModel();
if (options.relate) {
model.setRelate(options.relate);
}
const o = {
scope: this.name,
model: model,
returnUrl: options.returnUrl,
returnDispatchParams: options.returnDispatchParams,
params: options,
};
for (const k in optionsOptions) {
o[k] = optionsOptions[k];
}
await new DefaultsPopulator().populate(model);
if (options.attributes) {
model.set(options.attributes);
}
this.prepareModelCreate(model, options);
this.main(this.getViewName('edit'), o);
}
/**
* @param {{
* options?: Record,
* relate?: import('model').SetRelateItem | import('model').SetRelateItem [],
* returnUrl?: string,
* returnDispatchParams?: Record,
* options?: Record,
* attributes?: Record,
* } | Record} [options]
*/
actionCreate(options) {
this.create(options);
}
// noinspection JSUnusedGlobalSymbols
beforeEdit() {
this.handleCheckAccess('edit');
}
// noinspection JSUnusedLocalSymbols
/**
* @protected
* @param {module:model} model
* @param {Record} options
*/
prepareModelEdit(model, options) {
this.listenToOnce(model, 'before:save', () => {
const key = 'list';
const stored = this.getStoredMainView(key);
if (!stored) {
return;
}
if (!('storeViewAfterUpdate' in stored) || !stored.storeViewAfterUpdate) {
this.clearStoredMainView(key);
}
});
}
/**
* @param {{
* id: string,
* options?: Record,
* model?: import('model').default,
* returnUrl?: string,
* attributes?: Record,
* highlightFieldList?: string[],
* returnDispatchParams?: Record,
* }} options
*/
actionEdit(options) {
const id = options.id;
const optionsOptions = options.options || {};
this.getModel().then(model => {
model.id = id;
if (options.model) {
model = options.model;
}
this.prepareModelEdit(model, options);
this.showLoadingNotification();
model
.fetch({main: true})
.then(() => {
this.hideLoadingNotification();
const o = {
scope: this.name,
model: model,
returnUrl: options.returnUrl,
returnDispatchParams: options.returnDispatchParams,
params: options,
};
for (const k in optionsOptions) {
o[k] = optionsOptions[k];
}
if (options.attributes) {
o.attributes = options.attributes;
}
if (options.highlightFieldList) {
o.highlightFieldList = options.highlightFieldList;
}
this.main(this.getViewName('edit'), o);
});
this.listenToOnce(this.baseController, 'action', () => {
model.abortLastFetch();
});
});
}
// noinspection JSUnusedGlobalSymbols
beforeMerge() {
this.handleCheckAccess('edit');
}
// noinspection JSUnusedGlobalSymbols
/**
* @param {{
* ids: string,
* collection: import('collection').default,
* }} options
*/
actionMerge(options) {
const ids = options.ids.split(',');
this.getModel().then((model) => {
const models = [];
const proceed = () => {
this.main('views/merge', {
models: models,
scope: this.name,
collection: options.collection
});
};
let i = 0;
ids.forEach(id => {
const current = model.clone();
current.id = id;
models.push(current);
this.listenToOnce(current, 'sync', () => {
i++;
if (i === ids.length) {
proceed();
}
});
current.fetch();
});
});
}
// noinspection JSUnusedGlobalSymbols
/**
* @param {{
* id: string,
* link: string,
* }} options
*/
actionRelated(options) {
const id = options.id;
const link = options.link;
const viewName = this.getViewName('listRelated');
let model;
this.getModel()
.then(m => {
model = m;
model.id = id;
return model.fetch({main: true});
})
.then(() => {
const foreignEntityType = model.getLinkParam(link, 'entity');
if (!foreignEntityType) {
this.baseController.error404();
throw new Error(`Bad link '${link}'.`);
}
return this.collectionFactory.create(foreignEntityType);
})
.then(collection => {
collection.url = model.entityType + '/' + id + '/' + link;
this.main(viewName, {
scope: this.name,
model: model,
collection: collection,
link: link,
});
})
}
/**
* Get a collection for the current controller.
*
* @protected
* @param {boolean} [usePreviouslyFetched=false] Use a previously fetched. @todo Revise.
* @return {Promise<module:collection>}
*/
getCollection(usePreviouslyFetched) {
if (!this.name) {
throw new Error('No collection for unnamed controller');
}
const entityType = this.entityType || this.name;
if (usePreviouslyFetched && entityType in this.collectionMap) {
const collection = this.collectionMap[entityType];
return Promise.resolve(collection);
}
return this.collectionFactory.create(entityType, collection => {
this.collectionMap[entityType] = collection;
this.listenTo(collection, 'sync', () => collection.isFetched = true);
});
}
/**
* Get a model for the current controller.
*
* @protected
* @param {Function} [callback]
* @param {Object} [context]
* @return {Promise<module:model>}
*/
getModel(callback, context) {
context = context || this;
if (!this.name) {
throw new Error('No collection for unnamed controller');
}
const modelName = this.entityType || this.name;
return this.modelFactory.create(modelName, model => {
if (callback) {
callback.call(context, model);
}
});
}
}
export default RecordController;

View File

@@ -0,0 +1,42 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class RoleController extends RecordController {
checkAccess(action) {
if (this.getUser().isAdmin()) {
return true;
}
return false;
}
}
export default RoleController;

View File

@@ -0,0 +1,81 @@
/************************************************************************
* 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 Controller from 'controller';
class StreamController extends Controller {
defaultAction = 'index'
// noinspection JSUnusedGlobalSymbols
actionIndex() {
const key = 'index';
const isReturn = this.getRouter().backProcessed;
if (!isReturn) {
this.clearStoredMainView(key);
}
this.main('views/stream', {displayTitle: true}, undefined, {
key: key,
useStored: isReturn,
});
}
// noinspection JSUnusedGlobalSymbols
actionPosts() {
const key = 'index';
const isReturn = this.getRouter().backProcessed;
if (!isReturn) {
this.clearStoredMainView(key);
}
this.main('views/stream', {displayTitle: true, filter: 'posts'}, undefined, {
key: key,
useStored: isReturn,
});
}
// noinspection JSUnusedGlobalSymbols
actionUpdates() {
const key = 'index';
const isReturn = this.getRouter().backProcessed;
if (!isReturn) {
this.clearStoredMainView(key);
}
this.main('views/stream', {displayTitle: true, filter: 'updates'}, undefined, {
key: key,
useStored: isReturn,
});
}
}
export default StreamController;

View File

@@ -0,0 +1,46 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
class TeamController extends RecordController {
checkAccess(action) {
if (action === 'read') {
return true;
}
if (this.getUser().isAdmin()) {
return true;
}
return false;
}
}
export default TeamController;

View File

@@ -0,0 +1,94 @@
/************************************************************************
* 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 RecordController from 'controllers/record';
import Ui from 'ui';
import Ajax from 'ajax';
import UserConnectedAppsView from 'views/user/connected-apps';
class UserController extends RecordController {
getCollection(usePreviouslyFetched) {
return super.getCollection()
.then(collection => {
collection.data.userType = 'internal';
return collection;
});
}
/**
* @protected
* @param {Object} options
* @param {module:models/user} model
* @param {string} view
*/
createViewView(options, model, view) {
if (model.get('deleted')) {
view = 'views/deleted-detail';
super.createViewView(options, model, view);
return;
}
if (model.isPortal()) {
this.getRouter().dispatch('PortalUser', 'view', {id: model.id, model: model});
return;
}
if (model.isApi()) {
this.getRouter().dispatch('ApiUser', 'view', {id: model.id, model: model});
return;
}
super.createViewView(options, model, view);
}
// noinspection JSUnusedGlobalSymbols
/**
* @param {{
* id?: string,
* }} options
*/
async actionListConnectedApps(options) {
if (!options.id) {
throw new Error("No user ID.");
}
const view = new UserConnectedAppsView({
id: options.id,
});
this.main(view);
}
}
export default UserController;

459
client/src/date-time.js Normal file
View File

@@ -0,0 +1,459 @@
/************************************************************************
* 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.
************************************************************************/
/** @module date-time */
import moment from 'moment';
/**
* A date-time util.
*/
class DateTime {
constructor() {}
/**
* A system date format.
*
* @type {string}
*/
internalDateFormat = 'YYYY-MM-DD'
/**
* A system date-time format.
*
* @type {string}
*/
internalDateTimeFormat = 'YYYY-MM-DD HH:mm'
/**
* A system date-time format including seconds.
*
* @type {string}
*/
internalDateTimeFullFormat = 'YYYY-MM-DD HH:mm:ss'
/**
* A date format for a current user.
*
* @type {string}
*/
dateFormat = 'MM/DD/YYYY'
/**
* A time format for a current user.
*
* @type {string}
*/
timeFormat = 'HH:mm'
/**
* A time zone for a current user.
*
* @type {string|null}
*/
timeZone = null
/**
* A system time zone.
*
* @type {string}
*/
systemTimeZone
/**
* A week start for a current user.
*
* @type {Number}
*/
weekStart = 1
/** @private */
readableDateFormatMap = {
'DD.MM.YYYY': 'DD MMM',
'DD/MM/YYYY': 'DD MMM',
}
/** @private */
readableShortDateFormatMap = {
'DD.MM.YYYY': 'D MMM',
'DD/MM/YYYY': 'D MMM',
}
/**
* Whether a time format has a meridian (am/pm).
*
* @returns {boolean}
*/
hasMeridian() {
return (new RegExp('A', 'i')).test(this.timeFormat);
}
/**
* Get a date format.
*
* @returns {string}
*/
getDateFormat() {
return this.dateFormat;
}
/**
* Get a time format.
*
* @returns {string}
*/
getTimeFormat() {
return this.timeFormat;
}
/**
* Get a date-time format.
*
* @returns {string}
*/
getDateTimeFormat() {
return this.dateFormat + ' ' + this.timeFormat;
}
/**
* Get a readable date format.
*
* @returns {string}
*/
getReadableDateFormat() {
return this.readableDateFormatMap[this.getDateFormat()] || 'MMM DD';
}
/**
* Get a readable short date format.
*
* @returns {string}
*/
getReadableShortDateFormat() {
return this.readableShortDateFormatMap[this.getDateFormat()] || 'MMM D';
}
// noinspection JSUnusedGlobalSymbols
/**
* Get a readable date-time format.
*
* @returns {string}
*/
getReadableDateTimeFormat() {
return this.getReadableDateFormat() + ' ' + this.timeFormat;
}
/**
* Get a readable short date-time format.
*
* @returns {string}
*/
getReadableShortDateTimeFormat() {
return this.getReadableShortDateFormat() + ' ' + this.timeFormat;
}
/**
* Convert a date from a display representation to system.
*
* @param {string} string A date value.
* @returns {string|-1} A system date value.
*/
fromDisplayDate(string) {
const m = moment(string, this.dateFormat);
if (!m.isValid()) {
return -1;
}
return m.format(this.internalDateFormat);
}
/**
* Get a time-zone.
*
* @returns {string}
*/
getTimeZone() {
return this.timeZone ? this.timeZone : 'UTC';
}
/**
* Convert a date from system to a display representation.
*
* @param {string} string A system date value.
* @returns {string} A display date value.
*/
toDisplayDate(string) {
if (!string || (typeof string !== 'string')) {
return '';
}
const m = moment(string, this.internalDateFormat);
if (!m.isValid()) {
return '';
}
return m.format(this.dateFormat);
}
/**
* Convert a date-time from system to a display representation.
*
* @param {string} string A system date-time value.
* @returns {string|-1} A display date-time value.
*/
fromDisplay(string) {
let m;
if (this.timeZone) {
m = moment.tz(string, this.getDateTimeFormat(), this.timeZone).utc();
}
else {
m = moment.utc(string, this.getDateTimeFormat());
}
if (!m.isValid()) {
return -1;
}
return m.format(this.internalDateTimeFormat) + ':00';
}
/**
* Convert a date-time from system to a display representation.
*
* @param {string} string A system date value.
* @returns {string} A display date-time value.
*/
toDisplay(string) {
if (!string) {
return '';
}
return this.toMoment(string).format(this.getDateTimeFormat());
}
/**
* Get a now moment.
*
* @returns {moment.Moment}
*/
getNowMoment() {
return moment().tz(this.getTimeZone())
}
/**
* Convert a system-formatted date to a moment.
*
* @param {string} string A date value in a system representation.
* @returns {moment.Moment}
* @internal
*/
toMomentDate(string) {
return moment.tz(string, this.internalDateFormat, this.systemTimeZone);
}
/**
* Convert a system-formatted date-time to a moment.
*
* @param {string} string A date-time value in a system representation.
* @returns {moment.Moment}
* @internal
*/
toMoment(string) {
let m = moment.utc(string, this.internalDateTimeFullFormat);
if (this.timeZone) {
// noinspection JSUnresolvedReference
m = m.tz(this.timeZone);
}
return m;
}
/**
* Convert a date-time value from ISO to a system representation.
*
* @param {string} string
* @returns {string} A date-time value in a system representation.
*/
fromIso(string) {
if (!string) {
return '';
}
const m = moment(string).utc();
return m.format(this.internalDateTimeFormat);
}
// noinspection JSUnusedGlobalSymbols
/**
* Convert a date-time value from system to an ISO representation.
*
* @param string A date-time value in a system representation.
* @returns {string} An ISO date-time value.
*/
toIso(string) {
return this.toMoment(string).format();
}
/**
* Get a today date value in a system representation.
*
* @returns {string}
*/
getToday() {
return moment().tz(this.getTimeZone()).format(this.internalDateFormat);
}
/**
* Get a date-time value in a system representation, shifted from now.
*
* @param {Number} shift A number to shift by.
* @param {'minutes'|'hours'|'days'|'weeks'|'months'|'years'} type A shift unit.
* @param {Number} [multiplicity] A number of minutes a value will be aliquot to.
* @returns {string} A date-time value in a system representation
*/
getDateTimeShiftedFromNow(shift, type, multiplicity) {
if (!multiplicity) {
return moment.utc().add(shift, type).format(this.internalDateTimeFormat);
}
let unix = moment().unix();
unix = unix - (unix % (multiplicity * 60));
return moment.unix(unix).utc().add(shift, type).format(this.internalDateTimeFormat);
}
/**
* Get a date value in a system representation, shifted from today.
*
* @param {Number} shift A number to shift by.
* @param {'days'|'weeks'|'months'|'years'} type A shift unit.
* @returns {string} A date value in a system representation
*/
getDateShiftedFromToday(shift, type) {
return moment.tz(this.getTimeZone()).add(shift, type).format(this.internalDateFormat);
}
/**
* Get a now date-time value in a system representation.
*
* @param {Number} [multiplicity] A number of minutes a value will be aliquot to.
* @returns {string}
*/
getNow(multiplicity) {
if (!multiplicity) {
return moment.utc().format(this.internalDateTimeFormat);
}
let unix = moment().unix();
unix = unix - (unix % (multiplicity * 60));
return moment.unix(unix).utc().format(this.internalDateTimeFormat);
}
/**
* Set settings and preferences.
*
* @param {module:models/settings} settings Settings.
* @param {module:models/preferences} preferences Preferences.
* @internal
*/
setSettingsAndPreferences(settings, preferences) {
if (settings.has('dateFormat')) {
this.dateFormat = settings.get('dateFormat');
}
if (settings.has('timeFormat')) {
this.timeFormat = settings.get('timeFormat');
}
if (settings.has('timeZone')) {
this.timeZone = settings.get('timeZone') || null;
this.systemTimeZone = this.timeZone || 'UTC';
if (this.timeZone === 'UTC') {
this.timeZone = null;
}
}
if (settings.has('weekStart')) {
this.weekStart = settings.get('weekStart');
}
preferences.on('change', model => {
if (model.has('dateFormat') && model.get('dateFormat')) {
this.dateFormat = model.get('dateFormat');
}
if (model.has('timeFormat') && model.get('timeFormat')) {
this.timeFormat = model.get('timeFormat');
}
if (model.has('timeZone') && model.get('timeZone')) {
this.timeZone = model.get('timeZone');
}
if (model.has('weekStart') && model.get('weekStart') !== -1) {
this.weekStart = model.get('weekStart');
}
if (this.timeZone === 'UTC') {
this.timeZone = null;
}
});
}
/**
* Set a language.
*
* @param {module:language} language A language.
* @internal
*/
setLanguage(language) {
moment.updateLocale('en', {
months: language.translatePath(['Global', 'lists', 'monthNames']),
monthsShort: language.translatePath(['Global', 'lists', 'monthNamesShort']),
weekdays: language.translatePath(['Global', 'lists', 'dayNames']),
weekdaysShort: language.translatePath(['Global', 'lists', 'dayNamesShort']),
weekdaysMin: language.translatePath(['Global', 'lists', 'dayNamesMin']),
});
moment.locale('en');
}
}
export default DateTime;

72
client/src/di.js Normal file
View File

@@ -0,0 +1,72 @@
/************************************************************************
* 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.
************************************************************************/
const registry = new Map();
const container = new Map();
/**
* A DI container.
*/
export {container};
/**
* A 'register' decorator.
*
* @param {*[]} argumentList Arguments.
* @return {function(typeof Object)}
*/
export function register(argumentList = []) {
return function(classObject) {
registry.set(classObject, argumentList);
};
}
/**
* An 'inject' decorator.
*
* @param {any} classObject A class.
* @return {(function(*, Object): void)}
*/
export function inject(classObject) {
/**
* @param {{addInitializer: function(function())}} context
*/
return function(value, context) {
context.addInitializer(function() {
let instance = container.get(classObject);
if (!instance) {
instance = Reflect.construct(classObject, registry.get(classObject));
container.set(classObject, instance);
}
this[context.name] = instance;
});
};
}

View File

@@ -0,0 +1,90 @@
/************************************************************************
* 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.
************************************************************************/
/** @module dynamic-handler */
import {View as BullView} from 'bullbone';
/**
* A dynamic handler. To be extended by a specific handler.
*/
class DynamicHandler {
/**
* @param {module:views/record/detail} recordView A record view.
*/
constructor(recordView) {
/**
* A record view.
*
* @protected
* @type {module:views/record/detail}
*/
this.recordView = recordView;
/**
* A model.
*
* @protected
* @type {module:model}
*/
this.model = recordView.model;
}
/**
* Initialization logic. To be extended.
*
* @protected
*/
init() {}
/**
* Called on model change. To be extended.
*
* @protected
* @param {module:views/record/detail} model A model.
* @param {Object} o Options.
*/
onChange(model, o) {}
/**
* Get a metadata.
*
* @protected
* @returns {module:metadata}
*/
getMetadata() {
return this.recordView.getMetadata()
}
}
DynamicHandler.extend = BullView.extend;
// noinspection JSUnusedGlobalSymbols
export default DynamicHandler;

689
client/src/dynamic-logic.ts Normal file
View File

@@ -0,0 +1,689 @@
/************************************************************************
* 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 type BaseRecordView from 'views/record/base';
/**
* Definitions.
*/
export interface Defs {
/**
* Fields.
*/
fields?: Record<string, FieldDefs>;
/**
* Panels.
*/
panels?: Record<string, PanelDefs>;
/**
* Options.
*/
options?: Record<string, Record<string, any>[]>;
/**
* Cascading fields.
*/
cascadingFields?: Record<string, CascadingFieldDefs>;
}
/**
* Field definitions.
*/
interface FieldDefs {
/**
* Visibility conditions.
*/
visible?: { conditionGroup: ConditionGroup; };
/**
* Requiring conditions.
*/
required?: { conditionGroup: ConditionGroup; };
/**
* Read-only conditions.
*/
readOnly?: { conditionGroup: ConditionGroup; };
/**
* Read-only saved conditions.
*/
readOnlySaved?: { conditionGroup: ConditionGroup; };
/**
* Invalidity conditions.
*/
invalid?: { conditionGroup: ConditionGroup; };
}
/**
* Panel definitions.
*/
interface PanelDefs {
/**
* Visibility conditions.
*/
visible?: {
conditionGroup: ConditionGroup;
};
/**
* Style condition.
*/
styled?: {
conditionGroup: ConditionGroup;
};
}
interface CascadingFieldDefs {
items?: {
localField: string;
foreignField: string;
matchRequired: boolean;
}[];
}
type ConditionGroup = Record<string, unknown>[];
import {inject} from 'di';
import FieldManager from 'field-manager';
/**
* Dynamic logic. Handles form appearance and behavior depending on conditions.
*
* @internal Instantiated in advanced-pack.
*/
class DynamicLogic {
private defs: Defs
private recordView: BaseRecordView
private readonly fieldTypeList: ('visible' | 'required' | 'readOnlySaved' | 'readOnly')[] = [
'visible',
'required',
'readOnlySaved',
'readOnly',
]
private readonly panelTypeList: ('visible' | 'styled')[] = [
'visible',
'styled',
]
private readonly cascadingClearDefs: Record<string, string[]>
@inject(FieldManager)
private fieldManager: FieldManager
/**
* @param defs Definitions.
* @param recordView A record view.
*/
constructor(defs: Defs, recordView: BaseRecordView) {
this.defs = defs ?? {};
this.recordView = recordView;
this.cascadingClearDefs = this.buildCascadingClearDefs();
}
/**
* Process.
*
* @param options Options.
*/
process(options: {action?: string | 'ui'} = {}) {
const fields = this.defs.fields ?? {};
Object.keys(fields).forEach(field => {
const item = fields[field] || {};
let readOnlyIsProcessed = false;
this.fieldTypeList.forEach(type => {
if (!(type in item) || !item[type]) {
return;
}
const typeItem = item[type] || {} as {conditionGroup?: ConditionGroup};
if (!typeItem.conditionGroup) {
return;
}
if (type === 'readOnlySaved') {
if (this.recordView.model.isNew()) {
return;
}
if (this.checkConditionGroupInternal(typeItem.conditionGroup, 'and', true)) {
this.makeFieldReadOnlyTrue(field);
readOnlyIsProcessed = true;
} else {
this.makeFieldReadOnlyFalse(field);
}
return
}
const result = this.checkConditionGroupInternal(typeItem.conditionGroup);
if (type === 'required' && !readOnlyIsProcessed) {
result ?
this.makeFieldRequiredTrue(field) :
this.makeFieldRequiredFalse(field);
return;
}
if (type === 'readOnly') {
result ?
this.makeFieldReadOnlyTrue(field) :
this.makeFieldReadOnlyFalse(field);
return;
}
if (type === 'visible') {
result ?
this.makeFieldVisibleTrue(field) :
this.makeFieldVisibleFalse(field);
}
});
});
const panels = this.defs.panels ?? {};
Object.keys(panels).forEach(panel => {
this.panelTypeList.forEach(type => {
this.processPanel(panel, type);
});
});
const optionsDefs = this.defs.options ?? {};
Object.keys(optionsDefs).forEach(field => {
const itemList = optionsDefs[field];
if (!optionsDefs[field]) {
return;
}
let isMet = false;
for (const item of itemList) {
if (this.checkConditionGroupInternal(item.conditionGroup)) {
this.setOptionList(field, item.optionList || []);
isMet = true;
break;
}
}
if (!isMet) {
this.resetOptionList(field);
}
});
if (options.action === 'ui') {
this.processCascadingClear();
}
}
private buildCascadingClearDefs(): Record<string, string[]> {
const fields = this.defs.cascadingFields ?? null;
if (!fields || Object.keys(fields).length === 0) {
return {};
}
const model = this.recordView.model;
const map: Record<string, string[]> = {};
for (const [field, defs] of Object.entries(fields)) {
const items = defs?.items;
if (!items || !items.length) {
continue;
}
for (const item of items) {
if (!item.matchRequired) {
continue;
}
const type = model.getFieldType(item.localField);
const idAttribute = type === 'linkMultiple' ? item.localField + 'Ids' : item.localField + 'Id';
map[idAttribute] ??= [];
const attributeList = model.entityType ?
this.fieldManager.getEntityTypeFieldAttributeList(model.entityType, field) : [];
map[idAttribute].push(...attributeList);
}
}
for (const [attribute, a] of Object.entries(map)) {
map[attribute] = a.filter((it, i) => a.indexOf(it) === i);
}
return map;
}
private processCascadingClear() {
const attributeList = Object.keys(this.cascadingClearDefs);
const model = this.recordView.model;
const fieldsToUnset = [];
for (const attribute of attributeList) {
if (model.hasChanged(attribute)) {
fieldsToUnset.push(...this.cascadingClearDefs[attribute]);
}
}
if (!fieldsToUnset.length) {
return;
}
const map = fieldsToUnset.reduce((p, it) => {
p[it] = null;
return p;
}, {} as any)
setTimeout(() => {
model.setMultiple(map)
}, 0)
}
/**
* @param panel A panel name.
* @param type A type.
*/
private processPanel(panel: string, type: 'visible' | 'styled') {
const panels = this.defs.panels ?? {};
const item = (panels[panel] || {});
if (!(type in item)) {
return;
}
const typeItem = (item[type] ?? {}) as {conditionGroup?: ConditionGroup};
if (!typeItem.conditionGroup) {
return;
}
const result = this.checkConditionGroupInternal(typeItem.conditionGroup);
if (type === 'visible') {
result ?
this.makePanelVisibleTrue(panel) :
this.makePanelVisibleFalse(panel);
} if (type === 'styled') {
result ?
this.makePanelStyledTrue(panel) :
this.makePanelStyledFalse(panel);
}
}
/**
* Check a condition group.
*
* @param data A condition group.
* @returns {boolean}
*/
checkConditionGroup(data: ConditionGroup): boolean {
return this.checkConditionGroupInternal(data);
}
private checkConditionGroupInternal(
data: ConditionGroup,
type: 'and' | 'or' | 'not' = 'and',
preSave: boolean = false,
): boolean {
type = type || 'and';
let list: Defs[];
let result = false;
if (type === 'and') {
list = data || [];
result = true;
for (const it of list) {
if (!this.checkCondition(it, preSave)) {
result = false;
break;
}
}
} else if (type === 'or') {
list = data || [];
for (const it of list) {
if (this.checkCondition(it, preSave)) {
result = true;
break;
}
}
} else if (type === 'not') {
if (data) {
result = !this.checkCondition(data, preSave);
}
}
return result;
}
private getAttributeValue(attribute: string, preSave: boolean): unknown | undefined {
if (attribute.startsWith('$')) {
if (attribute === '$user.id') {
return this.recordView.getUser().id;
}
if (attribute === '$user.teamsIds') {
return this.recordView.getUser().getTeamIdList();
}
}
if (preSave) {
return this.recordView.getPreSaveAttributes()[attribute];
}
if (!this.recordView.model.has(attribute)) {
return undefined;
}
return this.recordView.model.get(attribute);
}
private checkCondition(defs: Record<string, any>, preSave: boolean): boolean {
defs = defs || {};
const type = defs.type || 'equals';
if (['or', 'and', 'not'].includes(type)) {
return this.checkConditionGroupInternal(defs.value, type, preSave);
}
const attribute = defs.attribute;
const value = defs.value;
if (!attribute) {
return false;
}
const setValue = this.getAttributeValue(attribute, preSave) as any;
if (type === 'equals') {
return setValue === value;
}
if (type === 'notEquals') {
return setValue !== value;
}
if (type === 'isEmpty') {
if (Array.isArray(setValue)) {
return !setValue.length;
}
return setValue === null || (setValue === '') || typeof setValue === 'undefined';
}
if (type === 'isNotEmpty') {
if (Array.isArray(setValue)) {
return !!setValue.length;
}
return setValue !== null && (setValue !== '') && typeof setValue !== 'undefined';
}
if (type === 'isTrue') {
return !!setValue;
}
if (type === 'isFalse') {
return !setValue;
}
if (type === 'contains' || type === 'has') {
if (!setValue || !hasIncludes(setValue)) {
return false;
}
return setValue.includes(value);
}
if (type === 'notContains' || type === 'notHas') {
if (!setValue || !hasIncludes(setValue)) {
return true;
}
return !setValue.includes(value);
}
if (type === 'startsWith') {
if (!setValue || !hasIndexOf(setValue)) {
return false;
}
return setValue.indexOf(value) === 0;
}
if (type === 'endsWith') {
if (!setValue || !hasIndexOf(setValue)) {
return false;
}
return setValue.indexOf(value) === (setValue as any).length - value.length;
}
if (type === 'matches') {
if (!setValue || typeof setValue !== 'string') {
return false;
}
const match = /^\/(.*)\/([a-z]*)$/.exec(value);
if (!match || match.length < 2) {
return false;
}
return (new RegExp(match[1], match[2])).test(setValue);
}
if (type === 'greaterThan') {
return setValue > value;
}
if (type === 'lessThan') {
return setValue < value;
}
if (type === 'greaterThanOrEquals') {
return setValue >= value;
}
if (type === 'lessThanOrEquals') {
return setValue <= value;
}
if (type === 'in') {
return !!~value.indexOf(setValue);
}
if (type === 'notIn') {
return !~value.indexOf(setValue);
}
if (type === 'isToday') {
const dateTime = this.recordView.getDateTime();
if (!setValue || typeof setValue !== 'string') {
return false;
}
if (setValue.length > 10) {
return dateTime.toMoment(setValue).isSame(dateTime.getNowMoment(), 'day');
}
return dateTime.toMomentDate(setValue).isSame(dateTime.getNowMoment(), 'day');
}
if (type === 'inFuture') {
const dateTime = this.recordView.getDateTime();
if (!setValue || typeof setValue !== 'string') {
return false;
}
if (setValue.length > 10) {
return dateTime.toMoment(setValue).isAfter(dateTime.getNowMoment(), 'second');
}
return dateTime.toMomentDate(setValue).isAfter(dateTime.getNowMoment(), 'day');
}
if (type === 'inPast') {
const dateTime = this.recordView.getDateTime();
if (!setValue || typeof setValue !== 'string') {
return false;
}
if (setValue.length > 10) {
return dateTime.toMoment(setValue).isBefore(dateTime.getNowMoment(), 'second');
}
return dateTime.toMomentDate(setValue).isBefore(dateTime.getNowMoment(), 'day');
}
return false;
}
private setOptionList(field: string, optionList: string[]) {
this.recordView.setFieldOptionList(field, optionList);
}
private resetOptionList(field: string) {
this.recordView.resetFieldOptionList(field);
}
private makeFieldVisibleTrue(field: string) {
this.recordView.showField(field);
}
private makeFieldVisibleFalse(field: string) {
this.recordView.hideField(field);
}
private makeFieldRequiredTrue(field: string) {
this.recordView.setFieldRequired(field);
}
private makeFieldRequiredFalse(field: string) {
this.recordView.setFieldNotRequired(field);
}
private makeFieldReadOnlyTrue(field: string) {
this.recordView.setFieldReadOnly(field);
}
private makeFieldReadOnlyFalse(field: string) {
this.recordView.setFieldNotReadOnly(field);
}
private makePanelVisibleTrue(panel: string) {
this.recordView.showPanel(panel, 'dynamicLogic');
}
private makePanelVisibleFalse(panel: string) {
this.recordView.hidePanel(panel, false, 'dynamicLogic');
}
private makePanelStyledTrue(panel: string) {
this.recordView.stylePanel(panel);
}
private makePanelStyledFalse(panel: string) {
this.recordView.unstylePanel(panel);
}
/**
* Add a panel-visible condition.
*
* @param name A panel name.
* @param item Condition definitions.
*/
addPanelVisibleCondition(name: string, item: {conditionGroup: ConditionGroup}) {
this.defs.panels = this.defs.panels ?? {};
this.defs.panels[name] = this.defs.panels[name] ?? {};
this.defs.panels[name].visible = item;
this.processPanel(name, 'visible');
}
/**
* Add a panel-styled condition.
*
* @param name A panel name.
* @param item Condition definitions.
*/
addPanelStyledCondition(name: string, item: {conditionGroup: ConditionGroup}) {
this.defs.panels = this.defs.panels ?? {};
this.defs.panels[name] = this.defs.panels[name] ?? {};
this.defs.panels[name].styled = item;
this.processPanel(name, 'styled');
}
}
export default DynamicLogic;
type HasIncludes = {
includes(value: any): boolean;
};
function hasIncludes(item: any): item is HasIncludes {
return typeof item?.includes === 'function';
}
type HasIndexOf = {
indexOf(value: any): number;
};
function hasIndexOf(item: any): item is HasIndexOf {
return typeof item?.indexOf === 'function';
}

510
client/src/email-helper.js Normal file
View File

@@ -0,0 +1,510 @@
/************************************************************************
* 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.
************************************************************************/
/** @module email-helper */
import {inject} from 'di';
import Language from 'language';
import User from 'models/user';
import DateTime from 'date-time';
import AclManager from 'acl-manager';
/**
* An email helper.
*/
class EmailHelper {
/**
* @private
* @type {Language}
*/
@inject(Language)
language
/**
* @private
* @type {User}
*/
@inject(User)
user
/**
* @private
* @type {DateTime}
*/
@inject(DateTime)
dateTime
/**
* @private
* @type {AclManager}
*/
@inject(AclManager)
acl
constructor() {
/** @private */
this.erasedPlaceholder = 'ERASED:';
}
/**
* Get reply email attributes.
*
* @param {module:model} model An email model.
* @param {boolean} [cc=false] To include CC (reply-all).
* @returns {Object.<string, *>}
*/
getReplyAttributes(model, 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 = '<br>' + '------' +
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 += '&lt;' + from + '&gt;';
}
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 += '&lt;' + to + '&gt;';
}
else {
line += '<' + to + '>';
}
partList.push(line);
});
line += partList.join(';');
list.push(line);
}
list.forEach(line => {
if (model.get('isHtml')) {
prepending += '<br>' + line;
}
else {
prepending += '\n' + line;
}
});
if (model.get('isHtml')) {
const body = model.get('body');
attributes['body'] = prepending + '<br><br>' + 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 <address@domain>`.
* @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 <address@domain>`.
* @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.<string, *>} 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'] = `<p data-quote-start="true"><br></p>` +
`<p>${replyHeadString}</p><blockquote>${body}</blockquote>`;
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;
}
}
export default EmailHelper;

63
client/src/exceptions.js Normal file
View File

@@ -0,0 +1,63 @@
/************************************************************************
* 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.
************************************************************************/
/** @module exceptions */
Espo.Exceptions = Espo.Exceptions || {};
/**
* An access-denied exception.
*
* @param {string} [message] A message.
* @class
*/
Espo.Exceptions.AccessDenied = function (message) {
this.message = message;
Error.apply(this, arguments);
};
Espo.Exceptions.AccessDenied.prototype = new Error();
Espo.Exceptions.AccessDenied.prototype.name = 'AccessDenied';
/**
* A not-found exception.
*
* @param {string} [message] A message.
* @class
*/
Espo.Exceptions.NotFound = function (message) {
this.message = message;
Error.apply(this, arguments);
};
Espo.Exceptions.NotFound.prototype = new Error();
Espo.Exceptions.NotFound.prototype.name = 'NotFound';
export default Espo.Exceptions;

458
client/src/field-manager.js Normal file
View File

@@ -0,0 +1,458 @@
/************************************************************************
* 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.
************************************************************************/
/** @module field-manager */
/**
* Utility for getting field related meta information.
*/
class FieldManager {
/**
* Utility for getting field related meta information.
*
* @param {Object} [defs] Field type definitions (metadata > fields).
* @param {module:metadata} [metadata] Metadata.
* @param {module:acl-manager} [acl] An ACL.
*/
constructor(defs, metadata, acl) {
/**
* @typedef {Object} FieldManager~defs
* @property {string[]} [actualFields]
* @property {string[]} [notActualFields]
* @property {'suffix'|'prefix'} [naming]
* @property {Object.<string, Object.<string, *>>} [params]
* @property {boolean} [filter]
* @property {boolean} [notMergeable]
* @property {string} [view]
*/
/**
* @public
* @internal
* @type {FieldManager~defs}
*/
this.defs = defs || /** @type {FieldManager~defs} */ {};
/**
* @public
* @internal
* @type {module:metadata}
*/
this.metadata = metadata;
/**
* @public
* @internal
* @type {module:acl-manager}
*/
this.acl = acl;
}
/**
* Get a list of parameters for a specific field type.
*
* @param {string} fieldType A field type.
* @returns {Object.<string, *>[]}
*/
getParamList(fieldType) {
if (fieldType in this.defs) {
return this.defs[fieldType].params || [];
}
return [];
}
/**
* Whether search filters are allowed for a field type.
*
* @param {string} fieldType A field type.
* @returns {boolean}
*/
checkFilter(fieldType) {
if (fieldType in this.defs) {
if ('filter' in this.defs[fieldType]) {
return this.defs[fieldType].filter;
}
return false;
}
return false;
}
/**
* Whether a merge operation is allowed for a field type.
*
* @param {string} fieldType A field type.
* @returns {boolean}
*/
isMergeable(fieldType) {
if (fieldType in this.defs) {
return !this.defs[fieldType].notMergeable;
}
return false;
}
/**
* Get a list of attributes of an entity type.
*
* @param {string} entityType An entity type.
* @param {module:field-manager~FieldFilters} [options] Filters.
* @returns {string[]}
*/
getEntityTypeAttributeList(entityType, options) {
const list = [];
const defs = this.metadata.get(`entityDefs.${entityType}.fields`) || {};
this.getEntityTypeFieldList(entityType, options).forEach(field => {
const fieldDefs = /** @type {Record} */defs[field] || {};
this.getAttributeList(fieldDefs.type, field).forEach(attr => {
if (!list.includes(attr)) {
list.push(attr);
}
});
});
return list;
}
/**
* Get a list of actual attributes by a given field type and field name.
* Non-actual attributes contains data that for a representation-only purpose.
* E.g. `accountId` is actual, `accountName` is non-actual.
*
* @param {string} fieldType A field type.
* @param {string} fieldName A field name.
* @returns {string[]}
*/
getActualAttributeList(fieldType, fieldName) {
const output = [];
if (!(fieldType in this.defs)) {
return [];
}
if ('actualFields' in this.defs[fieldType]) {
const actualFields = this.defs[fieldType].actualFields;
let naming = 'suffix';
if ('naming' in this.defs[fieldType]) {
naming = this.defs[fieldType].naming;
}
if (naming === 'prefix') {
actualFields.forEach(it => output.push(it + Espo.Utils.upperCaseFirst(fieldName)));
} else {
actualFields.forEach(it => output.push(fieldName + Espo.Utils.upperCaseFirst(it)));
}
} else {
output.push(fieldName);
}
return output;
}
/**
* Get a list of non-actual attributes by a given field type and field name.
* Non-actual attributes contains data that for a representation-only purpose.
* E.g. `accountId` is actual, `accountName` is non-actual.
*
* @param {string} fieldType A field type.
* @param {string} fieldName A field name.
* @returns {string[]}
*/
getNotActualAttributeList(fieldType, fieldName) {
if (!(fieldType in this.defs)) {
return [];
}
if (!('notActualFields' in this.defs[fieldType])) {
return [];
}
const notActualFields = this.defs[fieldType].notActualFields;
let naming = 'suffix';
if ('naming' in this.defs[fieldType]) {
naming = this.defs[fieldType].naming;
}
const output = [];
if (naming === 'prefix') {
notActualFields.forEach(it => {
if (it === '') {
output.push(fieldName);
} else {
output.push(it + Espo.Utils.upperCaseFirst(fieldName));
}
});
} else {
notActualFields.forEach(it => output.push(fieldName + Espo.Utils.upperCaseFirst(it)));
}
return output;
}
/**
* Get an attribute list of a specific field.
*
* @param {string} entityType An entity type.
* @param {string} field A field.
* @returns {string[]}
*/
getEntityTypeFieldAttributeList(entityType, field) {
const type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
if (!type) {
return [];
}
return _.union(
this.getAttributeList(type, field),
this._getEntityTypeFieldAdditionalAttributeList(entityType, field),
this._getEntityTypeFieldFullNameAdditionalAttributeList(entityType, field),
);
}
/**
* Get an actual attribute list of a specific field.
*
* @param {string} entityType An entity type.
* @param {string} field A field.
* @returns {string[]}
*/
getEntityTypeFieldActualAttributeList(entityType, field) {
const type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
if (!type) {
return [];
}
return _.union(
this.getActualAttributeList(type, field),
this._getEntityTypeFieldAdditionalAttributeList(entityType, field),
this._getEntityTypeFieldFullNameAdditionalAttributeList(entityType, field),
);
}
/**
* @private
*/
_getEntityTypeFieldAdditionalAttributeList(entityType, field) {
const type = this.metadata.get(['entityDefs', entityType, 'fields', field, 'type']);
if (!type) {
return [];
}
const partList = this.metadata
.get(['entityDefs', entityType, 'fields', field, 'additionalAttributeList']) || [];
if (partList.length === 0) {
return [];
}
const isPrefix = (this.defs[type] || {}).naming === 'prefix';
const list = [];
partList.forEach(item => {
if (isPrefix) {
list.push(item + Espo.Utils.upperCaseFirst(field));
return;
}
list.push(field + Espo.Utils.upperCaseFirst(item));
});
return list;
}
/**
* Get a list of attributes by a given field type and field name.
*
* @param {string} fieldType A field type.
* @param {string} fieldName A field name.
* @returns {string[]}
*/
getAttributeList(fieldType, fieldName) {
return _.union(
this.getActualAttributeList(fieldType, fieldName),
this.getNotActualAttributeList(fieldType, fieldName)
);
}
/**
* @typedef {Object} module:field-manager~FieldFilters
*
* @property {string} [type] Only of a specific field type.
* @property {string[]} [typeList] Only of a specific field types.
* @property {string[]} [ignoreTypeList] Ignore field types.
* @property {boolean} [onlyAvailable] To exclude disabled, admin-only, internal, forbidden fields.
* @property {'read'|'edit'} [acl] To exclude fields not accessible for a current user over
* a specified access level.
*/
/**
* Get a list of fields of a specific entity type.
*
* @param {string} entityType An entity type.
* @param {module:field-manager~FieldFilters} [options] Filters.
* @returns {string[]}
*/
getEntityTypeFieldList(entityType, options) {
/** @type {Record} */
const fieldDefs = this.metadata.get(['entityDefs', entityType, 'fields']) || {}
let list = Object.keys(fieldDefs);
options = options || {};
let typeList = options.typeList;
if (!typeList && options.type) {
typeList = [options.type];
}
if (typeList) {
list = list.filter(item => {
const type = this.metadata.get(['entityDefs', entityType, 'fields', item, 'type']);
return typeList.includes(type);
});
}
if (options.ignoreTypeList) {
list = list.filter(field => {
const type = (fieldDefs[field] || {}).type;
return !options.ignoreTypeList.includes(type);
});
}
if (options.onlyAvailable || options.acl) {
list = list.filter(item => {
return this.isEntityTypeFieldAvailable(entityType, item);
});
}
if (options.acl) {
const level = options.acl || 'read';
const forbiddenEditFieldList = this.acl.getScopeForbiddenFieldList(entityType, level);
list = list.filter(item => {
return !forbiddenEditFieldList.includes(item);
});
}
return list;
}
/**
* Get a field parameter value.
*
* @param {string} entityType An entity type.
* @param {string} field A field name.
* @param {string} param A parameter name.
* @returns {*}
*/
getEntityTypeFieldParam(entityType, field, param) {
return this.metadata.get(['entityDefs', entityType, 'fields', field, param]);
}
/**
* Get a view name/path for a specific field type.
*
* @param {string} fieldType A field type.
* @returns {string}
*/
getViewName(fieldType) {
if (fieldType in this.defs) {
if ('view' in this.defs[fieldType]) {
return this.defs[fieldType].view;
}
}
return 'views/fields/' + Espo.Utils.camelCaseToHyphen(fieldType);
}
/**
* Check whether a field is not disabled, not utility, not only-admin, not forbidden and not internal.
*
* @param {string} entityType An entity type.
* @param {string} field A field name.
* @returns {boolean}
*/
isEntityTypeFieldAvailable(entityType, field) {
/** @type {Record} */
const defs = this.metadata.get(['entityDefs', entityType, 'fields', field]) || {};
if (
defs.disabled ||
defs.utility
) {
return false;
}
/** @type {Record} */
const aclDefs = this.metadata.get(['entityAcl', entityType, 'fields', field]) || {};
if (
aclDefs.onlyAdmin ||
aclDefs.forbidden ||
aclDefs.internal
) {
return false;
}
return true;
}
/**
* @param {string} entityType
* @param {string} field
* @return {string[]}
* @private
*/
_getEntityTypeFieldFullNameAdditionalAttributeList(entityType, field) {
return this.metadata.get(`entityDefs.${entityType}.fields.${field}.fullNameAdditionalAttributeList`) ?? [];
}
}
export default FieldManager;

30
client/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,30 @@
export {};
import Ui from 'ui';
import Utils from 'utils';
/**
* A callback with resolved dependencies passed as parameters.
* Should return a value to define a module.
*
* @param {...any} arguments Resolved dependencies.
*/
type RequireCallback = (any) => void;
type RequireErrorCallback = () => void;
declare global {
namespace Espo {
// noinspection ES6ConvertVarToLetConst
var loader: {
require: (id: string, callback: RequireCallback, errorCallback?: RequireErrorCallback) => void;
requirePromise: <T = unknown>(id: string) => Promise<T>;
}
// noinspection ES6ConvertVarToLetConst
var Ui: Ui
// noinspection ES6ConvertVarToLetConst
var Utils: Utils
}
}

View File

@@ -0,0 +1,48 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
class PopulateDefaultsHandler extends ActionHandler {
async populate() {
const confirmMessage = this.view.translate('confirmPopulateDefaults', 'messages', 'AddressCountry');
await this.view.confirm(confirmMessage);
Espo.Ui.notifyWait();
await Espo.Ajax.postRequest('AddressCountry/action/populateDefaults');
await this.view.collection.fetch();
Espo.Ui.success(this.view.translate('Done'));
}
}
// noinspection JSUnusedGlobalSymbols
export default PopulateDefaultsHandler;

View File

@@ -0,0 +1,46 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
import {inject} from 'di';
import Router from 'router';
// noinspection JSUnusedGlobalSymbols
export default class extends ActionHandler {
/**
* @type {Router}
* @private
*/
@inject(Router)
router
process() {
window.open('api/v1/OpenApi', '_blank');
}
}

View File

@@ -0,0 +1,67 @@
/************************************************************************
* 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 type {CreateRelatedHandler as CreateRelatedHandlerInterface} from 'contracts/relation';
import type ViewHelper from 'view-helper';
import type Model from 'model';
/**
* Prepares attributes for a related record that is being created.
*/
abstract class CreateRelatedHandler<M extends Model = Model> implements CreateRelatedHandlerInterface<M> {
/**
* @internal
*/
protected readonly viewHelper: ViewHelper
/**
* @internal
*/
constructor(viewHelper: ViewHelper) {
this.viewHelper = viewHelper;
}
/**
* Get attributes for a new record.
*
* @param model A model.
* @param link A link name. As of v9.2.0.
* @return {Promise<Object.<string, unknown>>} Attributes.
*/
async getAttributes(model: M, link: string): Promise<Record<string, unknown>> {
// noinspection BadExpressionStatementJS
model;
// noinspection BadExpressionStatementJS
link;
return {};
}
}
export default CreateRelatedHandler;

View File

@@ -0,0 +1,65 @@
/************************************************************************
* 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 CreateRelatedHandler from 'handlers/create-related';
import {inject} from 'di';
import ModelFactory from 'model-factory';
export default class SetParentHandler extends CreateRelatedHandler {
/**
* @private
* @type {ModelFactory}
*/
@inject(ModelFactory)
modelFactory
async getAttributes(model, link) {
const entityType = model.getLinkParam(link, 'entity');
if (!entityType) {
return {};
}
const seed = await this.modelFactory.create(entityType);
/** @type {string[]} */
const parentEntityTypeList = seed.getFieldParam('parent', 'entityList') ?? [];
if (!parentEntityTypeList.includes(model.entityType)) {
return {};
}
return {
parentId: model.id,
parentName: model.attributes.name,
parentType: model.entityType,
}
}
}

View File

@@ -0,0 +1,49 @@
/************************************************************************
* 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 DefaultsPreparator from 'handlers/model/defaults-preparator';
import {inject} from 'di';
import Settings from 'models/settings';
// noinspection JSUnusedGlobalSymbols
export default class extends DefaultsPreparator {
/**
* @private
* @type {Settings}
*/
@inject(Settings)
config
async prepare(model) {
return {
baseCode: this.config.get('baseCurrency'),
};
}
}

View File

@@ -0,0 +1,44 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
export default class CurrencyRecordMenuActionSettingsHandler extends ActionHandler {
process() {
document.location.href = '#Admin/currency';
}
// noinspection JSUnusedGlobalSymbols
isVisible() {
/** @type {Record} */
const params = this.view.options?.params ?? {};
return params.fromSettings ?? false;
}
}

View File

@@ -0,0 +1,45 @@
/************************************************************************
* 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 CurrencyRecordRecordDetailHandler {
/**
* @param {import('views/record/detail').default} view
*/
constructor(view) {
this.view = view;
}
process() {
const model = this.view.model;
this.view.listenTo(model, 'after:relate:rates after:unrelate:rates after:related-change:rates', () => {
model.fetch();
});
}
}

View File

@@ -0,0 +1,54 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
import {inject} from 'di';
import Language from 'language';
import ResetFetchDataModalView from 'views/email-account/modals/reset-fetch-data';
export default class ResetFetchDataActionHandler extends ActionHandler {
/**
* @private
* @type {Language}
*/
@inject(Language)
language
async process() {
const model = this.view.model;
const view = new ResetFetchDataModalView({
model: model,
});
await this.view.assignView('dialog', view);
await view.render();
}
}

View File

@@ -0,0 +1,126 @@
/************************************************************************
* 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 DynamicHandler from 'dynamic-handler';
class EmailFilterHandler extends DynamicHandler {
init() {
if (this.model.isNew()) {
if (!this.recordView.getUser().isAdmin()) {
this.recordView.hideField('isGlobal');
}
}
if (
!this.model.isNew() &&
!this.recordView.getUser().isAdmin() &&
!this.model.get('isGlobal')
) {
this.recordView.hideField('isGlobal');
}
if (this.model.isNew() && !this.model.get('parentId')) {
this.model.set('parentType', 'User');
this.model.set('parentId', this.recordView.getUser().id);
this.model.set('parentName', this.recordView.getUser().get('name'));
if (!this.recordView.getUser().isAdmin()) {
this.recordView.setFieldReadOnly('parent');
}
}
else if (
this.model.get('parentType') &&
!this.recordView.options.duplicateSourceId
) {
this.recordView.setFieldReadOnly('parent');
this.recordView.setFieldReadOnly('isGlobal');
}
this.recordView.listenTo(this.model, 'change:isGlobal', (model, value, o) => {
if (!o.ui) {
return;
}
if (value) {
this.model.set({
action: 'Skip',
parentName: null,
parentType: null,
parentId: null,
emailFolderId: null,
groupEmailFolderId: null,
markAsRead: false,
});
}
});
this.recordView.listenTo(this.model, 'change:parentType', (model, value, o) => {
if (!o.ui) {
return;
}
// Avoiding side effects.
setTimeout(() => {
if (value !== 'User') {
this.model.set('markAsRead', false);
}
if (value === 'EmailAccount') {
this.model.set('action', 'Skip');
this.model.set('emailFolderId', null);
this.model.set('groupEmailFolderId', null);
this.model.set('markAsRead', false);
return;
}
if (value !== 'InboundEmail') {
if (this.model.get('action') === 'Move to Group Folder') {
this.model.set('action', 'Skip');
}
this.model.set('groupEmailFolderId', null);
return;
}
if (value !== 'User') {
if (this.model.get('action') === 'Move to Folder') {
this.model.set('action', 'Skip');
}
this.model.set('groupFolderId', null);
}
}, 40);
});
}
}
export default EmailFilterHandler;

View File

@@ -0,0 +1,54 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
import ImportEmlModal from 'views/email/modals/import-eml';
class EmailListActionsHandler extends ActionHandler {
// noinspection JSUnusedGlobalSymbols
async importEml() {
const view = new ImportEmlModal();
await this.view.assignView('dialog', view);
await view.render();
}
// noinspection JSUnusedGlobalSymbols
/**
* @return {boolean}
*/
checkImportEml() {
const acl = this.view.getAcl();
return acl.checkScope('Email', 'create') &&
acl.checkScope('Import');
}
}
export default EmailListActionsHandler;

View File

@@ -0,0 +1,53 @@
/************************************************************************
* 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 SelectRelatedHandler from 'handlers/select-related';
export default class extends SelectRelatedHandler {
/**
* @param {import('model').default} model
* @return {Promise<module:handlers/select-related~filters>}
*/
async getFilters(model) {
const acl = this.viewHelper.acl;
const permission = acl.getPermissionLevel('assignment');
/** @type {string[]} */
const boolFilterList = [];
if (permission === 'team') {
boolFilterList.push('onlyMyTeam');
} else if (permission === 'own') {
boolFilterList.push('onlyMe');
}
return {bool: boolFilterList};
}
}

View File

@@ -0,0 +1,51 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
class ImportHandler extends ActionHandler {
// noinspection JSUnusedGlobalSymbols
errorExport() {
Espo.Ajax
.postRequest(`Import/${this.view.model.id}/exportErrors`)
.then(data => {
if (!data.attachmentId) {
const message = this.view.translate('noErrors', 'messages', 'Import');
Espo.Ui.warning(message);
return;
}
window.location = this.view.getBasePath() + '?entryPoint=download&id=' + data.attachmentId;
});
}
}
export default ImportHandler;

View File

@@ -0,0 +1,45 @@
/************************************************************************
* 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 SelectRelatedHandler from 'handlers/select-related';
// noinspection JSUnusedGlobalSymbols
export default class extends SelectRelatedHandler {
async getFilters(model) {
return {
advanced: {
entityType: {
type: 'in',
attribute: 'entityType',
value: ['Lead'],
},
},
};
}
}

View File

@@ -0,0 +1,53 @@
/************************************************************************
* 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';
export default class LeadCaptureRecordDetailViewSetupHandler {
/**
* @private
* @type {Metadata}
*/
@inject(Metadata)
metadata
/**
* @param {import('views/record/detail').default} view
*/
constructor(view) {
this.view = view;
}
process() {
if (!this.metadata.get(`scopes.Lead.pipelines`)) {
this.view.hideField('pipeline', true);
}
}
}

View File

@@ -0,0 +1,70 @@
/************************************************************************
* 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.
************************************************************************/
/** @module handlers/login */
/**
* Custom login handling. To be extended.
*
* @abstract
*/
class LoginHandler {
/**
* @param {import('views/login').default} loginView A login view.
* @param {Object.<string, *>} data Additional metadata.
*/
constructor(loginView, data) {
/**
* A login view.
* @protected
* @type {import('views/login').default}
*/
this.loginView = loginView;
/**
* Additional metadata.
* @protected
* @type {Object.<string, *>}
*/
this.data = data;
}
/**
* Process. Called on 'Sign in' button click.
*
* @public
* @abstract
* @return {Promise<Object.<string, string>>} Resolved with headers to be sent to the `App/user` endpoint.
*/
process() {
return Promise.resolve({});
}
}
export default LoginHandler;

View File

@@ -0,0 +1,240 @@
/************************************************************************
* 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 LoginHandler from 'handlers/login';
import Base64 from 'js-base64';
class OidcLoginHandler extends LoginHandler {
/** @inheritDoc */
process() {
const proxy = window.open(
'about:blank',
'ConnectWithOAuthOidc',
'location=0,status=0,width=800,height=800'
);
Espo.Ui.notifyWait();
return new Promise((resolve, reject) => {
Espo.Ajax.getRequest('Oidc/authorizationData')
.then(data => {
Espo.Ui.notify(false);
this.processWithData(data, proxy)
.then(info => {
const code = info.code;
const nonce = info.nonce;
const authString = Base64.encode('**oidc:' + code);
const headers = {
'Espo-Authorization': authString,
'Authorization': 'Basic ' + authString,
'X-Oidc-Authorization-Nonce': nonce,
};
resolve(headers);
})
.catch(() => {
proxy.close();
reject();
});
})
.catch(() => {
Espo.Ui.notify(false)
proxy.close();
reject();
});
});
}
/**
* @private
* @param {{
* endpoint: string,
* clientId: string,
* redirectUri: string,
* scopes: string[],
* claims: string|null,
* prompt: 'login'|'consent'|'select_account',
* maxAge: Number|null,
* codeChallenge: string|null,
* codeChallengeMethod: string|null
* }} data
* @param {WindowProxy} proxy
* @return {Promise<{code: string, nonce: string}>}
*/
processWithData(data, proxy) {
const state = (Math.random() + 1).toString(36).substring(4);
const nonce = (Math.random() + 1).toString(36).substring(4);
const params = {
client_id: data.clientId,
redirect_uri: data.redirectUri,
response_type: 'code',
scope: data.scopes.join(' '),
state: state,
nonce: nonce,
prompt: data.prompt,
};
if (data.codeChallenge && data.codeChallengeMethod) {
params.code_challenge = data.codeChallenge;
params.code_challenge_method = data.codeChallengeMethod;
}
if (data.maxAge || data.maxAge === 0) {
params.max_age = data.maxAge;
}
if (data.claims) {
params.claims = data.claims;
}
const partList = Object.entries(params)
.map(([key, value]) => {
return key + '=' + encodeURIComponent(value);
});
const url = data.endpoint + '?' + partList.join('&');
return this.processWindow(url, state, nonce, proxy);
}
/**
* @private
* @param {string} url
* @param {string} state
* @param {string} nonce
* @param {WindowProxy} proxy
* @return {Promise<{code: string, nonce: string}>}
*/
processWindow(url, state, nonce, proxy) {
proxy.location.href = url;
return new Promise((resolve, reject) => {
const fail = () => {
window.clearInterval(interval);
if (!proxy.closed) {
proxy.close();
}
reject();
};
const interval = window.setInterval(() => {
if (proxy.closed) {
fail();
return;
}
let url;
try {
url = proxy.location.href;
} catch (e) {
return;
}
if (!url) {
return;
}
const parsedData = this.parseWindowUrl(url);
if (!parsedData) {
fail();
Espo.Ui.error('Could not parse URL', true);
return;
}
if ((parsedData.error || parsedData.code) && parsedData.state !== state) {
fail();
Espo.Ui.error('State mismatch', true);
return;
}
if (parsedData.error) {
fail();
const message = parsedData.errorDescription || this.loginView.translate('Error') + '\n' +
parsedData.error;
Espo.Ui.error(message, true);
console.log(parsedData);
return;
}
if (parsedData.code) {
window.clearInterval(interval);
proxy.close();
resolve({
code: parsedData.code,
nonce: nonce,
});
}
}, 300);
});
}
/**
* @param {string} url
* @return {?{
* code: ?string,
* state: ?string,
* error: ?string,
* errorDescription: ?string,
* }}
*/
parseWindowUrl(url) {
try {
const params = new URL(url).searchParams;
return {
code: params.get('code'),
state: params.get('state'),
error: params.get('error'),
errorDescription: params.get('errorDescription') ?? params.get('error_description'),
};
}
catch (e) {
return null;
}
}
}
export default OidcLoginHandler;

View File

@@ -0,0 +1,163 @@
/************************************************************************
* 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 MapRenderer from 'handlers/map/renderer';
class GoogleMapsRenderer extends MapRenderer {
/**
* @param {module:handlers/map/renderer~addressData} addressData
*/
render(addressData) {
if ('google' in window && window.google.maps) {
this.initMapGoogle(addressData);
return;
}
// noinspection SpellCheckingInspection
if (typeof window.mapapiloaded === 'function') {
// noinspection SpellCheckingInspection
const mapapiloaded = window.mapapiloaded;
// noinspection SpellCheckingInspection
window.mapapiloaded = () => {
this.initMapGoogle(addressData);
mapapiloaded();
};
return;
}
// noinspection SpellCheckingInspection
window.mapapiloaded = () => this.initMapGoogle(addressData);
let src = 'https://maps.googleapis.com/maps/api/js?callback=mapapiloaded&loading=async&v=weekly&libraries=marker';
const apiKey = this.view.getConfig().get('googleMapsApiKey');
if (apiKey) {
src += '&key=' + apiKey;
}
const scriptElement = document.createElement('script');
scriptElement.setAttribute('defer', 'defer');
scriptElement.src = src;
document.head.appendChild(scriptElement);
}
/**
* @param {module:handlers/map/renderer~addressData} addressData
*/
initMapGoogle(addressData) {
// noinspection JSUnresolvedReference
const geocoder = new google.maps.Geocoder();
let map;
const mapId = this.view.getConfig().get('googleMapsMapId') || 'DEMO_MAP_ID';
try {
// noinspection SpellCheckingInspection,JSUnresolvedReference
map = new google.maps.Map(this.view.$el.find('.map').get(0), {
zoom: 15,
center: {lat: 0, lng: 0},
scrollwheel: false,
mapId: mapId,
});
}
catch (e) {
console.error(e.message);
return;
}
let address = '';
if (addressData.street) {
address += addressData.street;
}
if (addressData.city) {
if (address !== '') {
address += ', ';
}
address += addressData.city;
}
if (addressData.state) {
if (address !== '') {
address += ', ';
}
address += addressData.state;
}
if (addressData.postalCode) {
if (addressData.state || addressData.city) {
address += ' ';
}
else {
if (address) {
address += ', ';
}
}
address += addressData.postalCode;
}
if (addressData.country) {
if (address !== '') {
address += ', ';
}
address += addressData.country;
}
// noinspection JSUnresolvedReference
geocoder.geocode({'address': address}, (results, status) => {
// noinspection JSUnresolvedReference
if (status === google.maps.GeocoderStatus.OK) {
// noinspection JSUnresolvedReference
map.setCenter(results[0].geometry.location);
// noinspection JSUnresolvedReference
new google.maps.marker.AdvancedMarkerElement({
map: map,
position: results[0].geometry.location,
});
}
});
}
}
// noinspection JSUnusedGlobalSymbols
export default GoogleMapsRenderer;

View File

@@ -0,0 +1,63 @@
/************************************************************************
* 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.
************************************************************************/
/**
* @module handlers/map/renderer
*/
/**
* A map renderer.
*
* @abstract
*/
class MapRenderer {
/**
* @typedef {Object} module:handlers/map/renderer~addressData
* @property {string|null} street
* @property {string|null} city
* @property {string|null} country
* @property {string|null} state
* @property {string|null} postalCode
*/
/**
* @param {import('views/fields/map').default} view A field view.
*/
constructor(view) {
this.view = view;
}
/**
* @param {module:handlers/map/renderer~addressData} addressData
* @abstract
*/
render(addressData) {}
}
export default MapRenderer;

View File

@@ -0,0 +1,55 @@
/************************************************************************
* 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.
************************************************************************/
/**
* @abstract
*/
class DefaultsPreparator {
/**
* @param {import('view-helper').default} viewHelper
*/
constructor(viewHelper) {
/** @protected */
this.viewHelper = viewHelper;
}
// noinspection JSUnusedLocalSymbols
/**
* Prepare default attributes.
*
* @param {import('model').default} model A model.
* @return {Promise<Object.<string, *>>}
*/
prepare(model) {
return Promise.resolve({});
}
}
// noinspection JSUnusedGlobalSymbols
export default DefaultsPreparator;

View File

@@ -0,0 +1,48 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
class NavbarMenuHandler extends ActionHandler {
logout() {
this.view.getRouter().logout();
}
// noinspection JSUnusedGlobalSymbols
lastViewed() {
Espo.Ui.notifyWait();
this.view.createView('dialog', 'views/modals/last-viewed', {}, view => {
view.render();
Espo.Ui.notify(false);
});
}
}
export default NavbarMenuHandler;

View File

@@ -0,0 +1,55 @@
/************************************************************************
* 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 {
/**
* @param {import('views/record/detail').default} view
*/
constructor(view) {
this.view = view;
}
process() {
this.controlFields();
this.view.listenTo(this.view.model, 'sync', () => this.controlFields());
}
controlFields() {
const attachmentsIds = this.view.model.attributes.attachmentsIds;
if (!attachmentsIds || !attachmentsIds.length) {
this.view.hideField('attachments');
return;
}
this.view.showField('attachments');
}
}

View File

@@ -0,0 +1,74 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
import MassActionHelper from 'helpers/mass-action';
import ListRecordView from 'views/record/list';
import Ui from 'ui';
import Ajax from 'ajax';
/**
* Important. Used for AccessToken, RefreshToken, AuthorizationCode.
*/
// noinspection JSUnusedGlobalSymbols
export default class RevokeMassActionHandler extends ActionHandler {
async process() {
const view = this.view as ListRecordView;
const helper = new MassActionHelper(this.view);
const params = view.getMassActionSelectionPostData();
const idle = !!params.searchParams && helper.checkIsIdle(view.collection.total);
const onDone = (count: number) => {
const msg = this.view.translate('massRevokeDone', 'messages', 'OAuthAccessToken')
.replace('{count}', count.toString());
Ui.success(msg);
};
Ui.notifyWait();
const result = await Ajax.postRequest('MassAction', {
entityType: view.collection.entityType!,
action: 'revoke',
params: params,
idle: idle,
});
if (result.id) {
const view = await helper.process(result.id, 'revoke')
this.view.listenToOnce(view, 'close:success', result => onDone(result.count));
return;
}
onDone(result.count);
}
}

View File

@@ -0,0 +1,58 @@
/************************************************************************
* 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 RowActionHandler from 'handlers/row-action';
import Model from 'model';
import Ajax from 'ajax';
import Ui from 'ui';
import {inject} from 'di';
import Language from 'language';
/**
* Important. Used for AccessToken, RefreshToken, AuthorizationCode.
*/
// noinspection JSUnusedGlobalSymbols
export default class RevokeRowActionHandler extends RowActionHandler {
@inject(Language)
private language: Language
async process(model: Model, _action: string) {
Ui.notifyWait();
await Ajax.postRequest(`${model.entityType}/${model.id}/revoke`);
await model.fetch();
Ui.success(this.language.translate('Done'));
}
isAvailable(model: Model, _action: string): boolean {
return model.attributes.status === 'Active';
}
}

View File

@@ -0,0 +1,92 @@
/************************************************************************
* 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 RowActionHandler from 'handlers/row-action';
export default class PipelineStageMoveRowActionHandler extends RowActionHandler
{
isAvailable(model, action) {
const collection = model.collection;
if (!collection) {
return false;
}
if (action === 'moveUp') {
if (collection.indexOf(model) <= 0) {
return false;
}
const index = collection.models.findIndex(m => m === model);
const prevModel = this.collection.at(index - 1);
if (!prevModel || prevModel.attributes.mappedStatus !== model.attributes.mappedStatus) {
return false;
}
}
if (action === 'moveDown') {
if (collection.indexOf(model) >= collection.total - 1) {
return false;
}
const index = collection.models.findIndex(m => m === model);
const nextModel = this.collection.at(index + 1);
if (!nextModel || nextModel.attributes.mappedStatus !== model.attributes.mappedStatus) {
return false;
}
}
return collection.orderBy === 'order' && collection.order === 'asc';
}
async process(model, action) {
let type;
if (action === 'moveUp') {
type = 'up';
} else if (action === 'moveDown') {
type = 'down';
} else {
throw new Error();
}
Espo.Ui.notifyWait();
await Espo.Ajax.postRequest(`${model.entityType}/${model.id}/move/${type}`, {
whereGroup: this.collection.getWhere(),
});
await this.collection.fetch();
Espo.Ui.notify();
}
}

View File

@@ -0,0 +1,80 @@
/************************************************************************
* 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 RowActionHandler from 'handlers/row-action';
export default class PipelineMoveRowActionHandler extends RowActionHandler {
isAvailable(model, action) {
const collection = model.collection;
if (!collection) {
return false;
}
if (action === 'moveToTop' || action === 'moveUp') {
if (collection.indexOf(model) <= 0) {
return false;
}
}
if (action === 'moveToBottom' || action === 'moveDown') {
if (collection.indexOf(model) >= collection.total - 1) {
return false;
}
}
return collection.orderBy === 'order' && collection.order === 'asc';
}
async process(model, action) {
let type;
if (action === 'moveToTop') {
type = 'top';
} else if (action === 'moveToBottom') {
type = 'bottom';
} else if (action === 'moveUp') {
type = 'up';
} else if (action === 'moveDown') {
type = 'down';
} else {
throw new Error();
}
Espo.Ui.notifyWait();
await Espo.Ajax.postRequest(`${model.entityType}/${model.id}/move/${type}`, {
whereGroup: this.collection.getWhere(),
});
await this.collection.fetch();
Espo.Ui.notify();
}
}

View File

@@ -0,0 +1,147 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
import {inject} from 'di';
import AclManager from 'acl-manager';
import Metadata from 'metadata';
// noinspection JSUnusedGlobalSymbols
export default class LockActionHandler extends ActionHandler {
/**
* @type {AclManager}
*/
@inject(AclManager)
acl
/**
* @type {Metadata}
*/
@inject(Metadata)
metadata
async actionLock() {
const model = this.view.model;
Espo.Ui.notifyWait();
let attributes;
try {
attributes = await Espo.Ajax.postRequest('Action', {
action: 'lock',
entityType: model.entityType,
id: model.id,
});
} catch (e) {
return;
}
model.setMultiple(attributes, {sync: true});
model.trigger('sync', this.model, null, {});
Espo.Ui.success(this.view.translate('locked', 'messages'));
}
async actionUnlock() {
const model = this.view.model;
Espo.Ui.notifyWait();
let attributes;
try {
attributes = await Espo.Ajax.postRequest('Action', {
action: 'unlock',
entityType: model.entityType,
id: model.id,
});
} catch (e) {
return;
}
model.setMultiple(attributes, {sync: true});
model.trigger('sync', this.model, null, {});
Espo.Ui.success(this.view.translate('unlocked', 'messages'));
}
// noinspection JSUnusedGlobalSymbols
/**
* @return {boolean}
*/
canBeLocked() {
const model = this.view.model;
if (!this.isEnabled(model.entityType)) {
return false;
}
if (this.acl.getPermissionLevel('lock') !== 'yes') {
return false;
}
if (model.attributes.isLocked) {
return false;
}
return true;
}
// noinspection JSUnusedGlobalSymbols
/**
* @return {boolean}
*/
canBeUnlocked() {
const model = this.view.model;
if (!this.isEnabled(model.entityType)) {
return false;
}
if (this.acl.getPermissionLevel('lock') !== 'yes') {
return false;
}
if (!model.attributes.isLocked) {
return false;
}
return true;
}
/**
* @private
* @param {string} entityType
* @return {boolean}
*/
isEnabled(entityType) {
return this.metadata.get(`scopes.${entityType}.lockable`) === true;
}
}

View File

@@ -0,0 +1,145 @@
/************************************************************************
* 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 ActionHandler from 'action-handler';
import {inject} from 'di';
import AclManager from 'acl-manager';
import Language from 'language';
import MassActionHelper from 'helpers/mass-action';
import Metadata from 'metadata';
// noinspection JSUnusedGlobalSymbols
export default class LockMassActionHandler extends ActionHandler {
/**
* @type {AclManager}
*/
@inject(AclManager)
acl
/**
* @type {Language}
*/
@inject(Language)
language
/**
* @type {Metadata}
*/
@inject(Metadata)
metadata
// noinspection JSUnusedGlobalSymbols
async actionLock() {
const msg = this.language.translate('confirmMassLock', 'messages');
await this.view.confirm(msg);
await this.process('lock');
}
// noinspection JSUnusedGlobalSymbols
async actionUnlock() {
const msg = this.language.translate('confirmMassUnlock', 'messages');
await this.view.confirm(msg);
await this.process('unlock');
}
/**
* @private
* @param {string} action
*/
async process(action) {
const helper = new MassActionHelper(this.view);
const params = this.view.getMassActionSelectionPostData();
const idle = !!params.searchParams && helper.checkIsIdle(this.view.collection.total);
const onDone = count => {
const labelKey = action === 'lock' ? 'massLockDone': 'massUnlockDone';
const msg = this.view.translate(labelKey, 'messages')
.replace('{count}', count.toString());
Espo.Ui.success(msg);
};
Espo.Ui.notifyWait();
const result = await Espo.Ajax.postRequest('MassAction', {
entityType: this.view.entityType,
action: action,
params: params,
idle: idle,
});
if (result.id) {
const view = await helper.process(result.id, action)
this.view.listenToOnce(view, 'close:success', result => onDone(result.count));
return;
}
onDone(result.count);
}
// noinspection JSUnusedGlobalSymbols
initLock() {
if (
!this.view.collection ||
!this.isEnabled(this.view.collection.entityType) ||
this.acl.getPermissionLevel('massUpdate') !== 'yes' ||
this.acl.getPermissionLevel('lock') !== 'yes'
) {
this.view.removeMassAction('lock');
}
}
// noinspection JSUnusedGlobalSymbols
initUnlock() {
if (
!this.view.collection ||
!this.isEnabled(this.view.collection.entityType) ||
this.acl.getPermissionLevel('massUpdate') !== 'yes' ||
this.acl.getPermissionLevel('lock') !== 'yes'
) {
this.view.removeMassAction('unlock');
}
}
/**
* @private
* @param {string} entityType
* @return {boolean}
*/
isEnabled(entityType) {
return this.metadata.get(`scopes.${entityType}.lockable`) === true;
}
}

View File

@@ -0,0 +1,163 @@
/************************************************************************
* 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 AssignmentHelper from 'helpers/assignment';
// noinspection JSUnusedGlobalSymbols
export default class LockViewSetup {
/**
* @private
* @type {Metadata}
*/
@inject(Metadata)
metadata
/**
* @private
* @type {import('model').default}
*/
model
/**
* @private
* @type {import('views/record/detail').default}
*/
view
ignoreFieldList = [
'isLocked',
'modifiedAt',
'modifiedBy',
'streamUpdatedAt',
]
/**
* @param {import('views/record/detail').default} view
*/
constructor(view) {
this.view = view;
this.model = view.model;
}
process() {
const entityType = this.model.entityType;
if (this.metadata.get(`scopes.${entityType}.lockable`) !== true) {
return;
}
let wasLocked = false;
const lockedMap = {};
const ignoreFieldList = this.getIgnoreFieldList();
const fieldsDefs = /** @type {Record.<string, Record>} */
this.metadata.get(`entityDefs.${entityType}.fields`) ?? {};
const lockableFields = Object.keys(fieldsDefs)
.filter(field => {
if (ignoreFieldList.includes(field)) {
return false;
}
const defs = fieldsDefs[field];
return !defs.notLockable &&
!defs.readOnly &&
!defs.readOnlyAfterCreate;
});
const controlLocked = () => {
const isLocked = this.model.attributes.isLocked;
if (!isLocked && !wasLocked) {
return;
}
if (isLocked) {
wasLocked = true;
}
lockableFields.forEach(field => {
if (isLocked) {
if (
this.view.recordHelper &&
this.view.recordHelper.getFieldStateParam(field, 'readOnly')
) {
return;
}
lockedMap[field] = true;
this.view.setFieldReadOnly(field);
return;
}
if (!lockedMap[field]) {
return;
}
delete lockedMap[field];
this.view.setFieldNotReadOnly(field);
});
}
controlLocked();
this.view.listenTo(this.model, 'change:isLocked', () => controlLocked());
}
/**
* @private
* @return {string[]}
*/
getIgnoreFieldList() {
const entityType = this.model.entityType;
const ignoreFieldList = [...this.ignoreFieldList];
const helper = new AssignmentHelper;
if (helper.hasCollaboratorsField(entityType)) {
if (helper.hasAssignedUsersField(entityType)) {
if (this.model.getFieldParam('assignedUsers', 'notLockable')) {
ignoreFieldList.push('collaborators');
}
} else if (helper.hasAssignedUserField(entityType)) {
if (this.model.getFieldParam('assignedUser', 'notLockable')) {
ignoreFieldList.push('collaborators');
}
}
}
return ignoreFieldList;
}
}

View File

@@ -0,0 +1,145 @@
/************************************************************************
* 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 AppParams from 'app-params';
import PipelinesHelper from 'helpers/misc/pipelines';
// noinspection JSUnusedGlobalSymbols
export default class PipelineViewSetupHandler {
/**
* @private
* @type {Metadata}
*/
@inject(Metadata)
metadata
/**
* @private
* @type {import('model').default}
*/
model
/**
* @private
* @type {import('views/record/detail').default}
*/
view
/**
* @private
* @type {AppParams}
*/
@inject(AppParams)
appParams
/**
* @private
* @type {{id: string, stages: {id: string, name: string, style: string|null}[]}[]}
*/
pipelines
/**
* @param {import('views/record/detail').default} view
*/
constructor(view) {
this.view = view;
this.model = view.model;
}
process() {
const entityType = this.model.entityType;
if (this.metadata.get(`scopes.${entityType}.pipelines`) !== true) {
return;
}
this.pipelines = new PipelinesHelper().get(entityType);
this.model.onChange({
owner: this.view,
attributes: ['pipelineId'],
callback: o => {
if (!o.ui) {
return;
}
setTimeout(() => this.updateStage(), 1);
},
});
}
/**
* @private
*/
updateStage() {
const pipelineId = this.model.attributes.pipelineId;
if (!pipelineId) {
this.setStageNull();
return;
}
const pipeline = this.pipelines.find(it => it.id === pipelineId);
if (!pipeline) {
this.setStageNull();
return;
}
const stage = pipeline.stages[0];
if (!stage) {
this.setStageNull();
return;
}
this.model.setMultiple({
pipelineStageId: stage.id,
pipelineStageName: stage.name,
});
this.model.trigger('pipeline-changed');
}
/**
* @private
*/
setStageNull() {
this.model.setMultiple({
pipelineStageId: null,
pipelineStageName: null,
});
this.model.trigger('pipeline-changed');
}
}

View File

@@ -0,0 +1,72 @@
/************************************************************************
* 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 StreamViewAuditLogModalView from 'views/stream/modals/view-audit-log';
class ViewAuditLogHandler {
constructor(/** import('views/record/detail').default */view) {
this.view = view;
this.metadata = /** @type {module:metadata} */view.getMetadata();
this.entityType = this.view.entityType;
this.model = /** @type {module:model} */this.view.model;
this.hasAudited =
this.metadata.get(`scopes.${this.entityType}.statusField`) ||
this.model.getFieldList().find(field => this.model.getFieldParam(field, 'audited')) !== undefined;
if (this.entityType === 'User' && !this.view.getUser().isAdmin()) {
this.hasAudited = false;
}
if (this.view.getUser().isPortal()) {
this.hasAudited = false;
}
if (this.view.getAcl().getPermissionLevel('audit') !== 'yes') {
this.hasAudited = false;
}
}
isAvailable() {
return this.hasAudited;
}
show() {
const view = new StreamViewAuditLogModalView({model: this.model});
this.view.assignView('dialog', view)
.then(() => {
view.render();
})
}
}
// noinspection JSUnusedGlobalSymbols
export default ViewAuditLogHandler;

Some files were not shown because too many files have changed in this diff Show More