snapshot of https://github.com/espocrm/espocrm.git (master, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Test / Test on PHP 8.3 (push) Failing after 5s
Test / Test on PHP 8.4 (push) Failing after 4s
Test / Test on PHP 8.5 (push) Failing after 3s
TypeScript static analysis / TypeScript check (push) Failing after 14s
Test Integration on MySQL / Test on PHP 8.3 (push) Failing after 1s
Test Integration on MySQL / Test on PHP 8.5 (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 fix (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 master (push) Failing after 1s
Some checks failed
Test / Test on PHP 8.3 (push) Failing after 5s
Test / Test on PHP 8.4 (push) Failing after 4s
Test / Test on PHP 8.5 (push) Failing after 3s
TypeScript static analysis / TypeScript check (push) Failing after 14s
Test Integration on MySQL / Test on PHP 8.3 (push) Failing after 1s
Test Integration on MySQL / Test on PHP 8.5 (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 fix (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 master (push) Failing after 1s
This commit is contained in:
508
client/src/acl-manager.js
Normal file
508
client/src/acl-manager.js
Normal 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;
|
||||
91
client/src/acl-portal-manager.js
Normal file
91
client/src/acl-portal-manager.js
Normal 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
394
client/src/acl-portal.js
Normal 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;
|
||||
64
client/src/acl-portal/email.js
Normal file
64
client/src/acl-portal/email.js
Normal 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;
|
||||
42
client/src/acl-portal/notification.js
Normal file
42
client/src/acl-portal/notification.js
Normal 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;
|
||||
42
client/src/acl-portal/preferences.js
Normal file
42
client/src/acl-portal/preferences.js
Normal 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
384
client/src/acl.js
Normal 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;
|
||||
40
client/src/acl/currency-record-rate.js
Normal file
40
client/src/acl/currency-record-rate.js
Normal 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
121
client/src/acl/email.js
Normal 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
45
client/src/acl/foreign.js
Normal 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
55
client/src/acl/import.js
Normal 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;
|
||||
42
client/src/acl/notification.js
Normal file
42
client/src/acl/notification.js
Normal 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;
|
||||
42
client/src/acl/preferences.js
Normal file
42
client/src/acl/preferences.js
Normal 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
40
client/src/acl/team.js
Normal 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
50
client/src/acl/user.js
Normal 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;
|
||||
52
client/src/action-handler.js
Normal file
52
client/src/action-handler.js
Normal 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
355
client/src/ajax.ts
Normal 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
73
client/src/app-params.js
Normal 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
51
client/src/app-portal.js
Normal 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
1639
client/src/app.js
Normal file
File diff suppressed because it is too large
Load Diff
74
client/src/broadcast-channel.js
Normal file
74
client/src/broadcast-channel.js
Normal 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
222
client/src/cache.js
Normal 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;
|
||||
100
client/src/collection-factory.ts
Normal file
100
client/src/collection-factory.ts
Normal 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
1341
client/src/collection.ts
Normal file
File diff suppressed because it is too large
Load Diff
146
client/src/collections/note.js
Normal file
146
client/src/collections/note.js
Normal 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;
|
||||
34
client/src/collections/notification.js
Normal file
34
client/src/collections/notification.js
Normal 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
|
||||
}
|
||||
50
client/src/collections/o-auth-client-secret.ts
Normal file
50
client/src/collections/o-auth-client-secret.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
169
client/src/collections/tree.js
Normal file
169
client/src/collections/tree.js
Normal 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;
|
||||
276
client/src/components/controls.ts
Normal file
276
client/src/components/controls.ts
Normal 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};
|
||||
}
|
||||
107
client/src/contracts/relation.ts
Normal file
107
client/src/contracts/relation.ts
Normal 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
789
client/src/controller.js
Normal 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;
|
||||
43
client/src/controllers/about.js
Normal file
43
client/src/controllers/about.js
Normal 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;
|
||||
64
client/src/controllers/address-map.js
Normal file
64
client/src/controllers/address-map.js
Normal 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;
|
||||
579
client/src/controllers/admin.js
Normal file
579
client/src/controllers/admin.js
Normal 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;
|
||||
75
client/src/controllers/api-user.js
Normal file
75
client/src/controllers/api-user.js
Normal 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;
|
||||
235
client/src/controllers/base.js
Normal file
235
client/src/controllers/base.js
Normal 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;
|
||||
45
client/src/controllers/dashboard.js
Normal file
45
client/src/controllers/dashboard.js
Normal 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;
|
||||
47
client/src/controllers/email-filter.js
Normal file
47
client/src/controllers/email-filter.js
Normal 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;
|
||||
47
client/src/controllers/email.js
Normal file
47
client/src/controllers/email.js
Normal 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;
|
||||
67
client/src/controllers/external-account.js
Normal file
67
client/src/controllers/external-account.js
Normal 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;
|
||||
52
client/src/controllers/global-stream.js
Normal file
52
client/src/controllers/global-stream.js
Normal 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;
|
||||
39
client/src/controllers/home.js
Normal file
39
client/src/controllers/home.js
Normal 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;
|
||||
110
client/src/controllers/import.js
Normal file
110
client/src/controllers/import.js
Normal 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;
|
||||
42
client/src/controllers/inbound-email.js
Normal file
42
client/src/controllers/inbound-email.js
Normal 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;
|
||||
41
client/src/controllers/last-viewed.js
Normal file
41
client/src/controllers/last-viewed.js
Normal 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;
|
||||
52
client/src/controllers/layout-set.js
Normal file
52
client/src/controllers/layout-set.js
Normal 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;
|
||||
59
client/src/controllers/lead-capture-form.js
Normal file
59
client/src/controllers/lead-capture-form.js
Normal 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);
|
||||
}
|
||||
}
|
||||
59
client/src/controllers/lead-capture-opt-in-confirmation.js
Normal file
59
client/src/controllers/lead-capture-opt-in-confirmation.js
Normal 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;
|
||||
60
client/src/controllers/login-as.js
Normal file
60
client/src/controllers/login-as.js
Normal 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;
|
||||
66
client/src/controllers/note.js
Normal file
66
client/src/controllers/note.js
Normal 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;
|
||||
43
client/src/controllers/notification.js
Normal file
43
client/src/controllers/notification.js
Normal 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;
|
||||
42
client/src/controllers/o-auth-authorize.js
Normal file
42
client/src/controllers/o-auth-authorize.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
51
client/src/controllers/password-change-request.js
Normal file
51
client/src/controllers/password-change-request.js
Normal 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;
|
||||
42
client/src/controllers/portal-role.js
Normal file
42
client/src/controllers/portal-role.js
Normal 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;
|
||||
83
client/src/controllers/portal-user.js
Normal file
83
client/src/controllers/portal-user.js
Normal 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;
|
||||
64
client/src/controllers/preferences.js
Normal file
64
client/src/controllers/preferences.js
Normal 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;
|
||||
95
client/src/controllers/record-tree.js
Normal file
95
client/src/controllers/record-tree.js
Normal 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;
|
||||
590
client/src/controllers/record.js
Normal file
590
client/src/controllers/record.js
Normal 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;
|
||||
42
client/src/controllers/role.js
Normal file
42
client/src/controllers/role.js
Normal 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;
|
||||
81
client/src/controllers/stream.js
Normal file
81
client/src/controllers/stream.js
Normal 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;
|
||||
46
client/src/controllers/team.js
Normal file
46
client/src/controllers/team.js
Normal 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;
|
||||
94
client/src/controllers/user.js
Normal file
94
client/src/controllers/user.js
Normal 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
459
client/src/date-time.js
Normal 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
72
client/src/di.js
Normal 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;
|
||||
});
|
||||
};
|
||||
}
|
||||
90
client/src/dynamic-handler.js
Normal file
90
client/src/dynamic-handler.js
Normal 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
689
client/src/dynamic-logic.ts
Normal 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
510
client/src/email-helper.js
Normal 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 += '<' + from + '>';
|
||||
}
|
||||
else {
|
||||
line += '<' + from + '>';
|
||||
}
|
||||
|
||||
list.push(line);
|
||||
}
|
||||
|
||||
if (model.get('dateSent')) {
|
||||
let line = this.language.translate('dateSent', 'fields', 'Email') + ': ';
|
||||
line += this.dateTime.toDisplay(model.get('dateSent'));
|
||||
|
||||
list.push(line);
|
||||
}
|
||||
|
||||
if (model.get('name')) {
|
||||
let line = this.language.translate('subject', 'fields', 'Email') + ': ';
|
||||
|
||||
line += model.get('name');
|
||||
|
||||
list.push(line);
|
||||
}
|
||||
|
||||
if (model.get('to')) {
|
||||
let line = this.language.translate('to', 'fields', 'Email') + ': ';
|
||||
|
||||
const partList = [];
|
||||
|
||||
model.get('to').split(';').forEach(to => {
|
||||
const nameHash = model.get('nameHash') || {};
|
||||
let line = '';
|
||||
|
||||
if (to in nameHash) {
|
||||
line += nameHash[to] + ' ';
|
||||
}
|
||||
|
||||
if (model.get('isHtml')) {
|
||||
line += '<' + to + '>';
|
||||
}
|
||||
else {
|
||||
line += '<' + to + '>';
|
||||
}
|
||||
|
||||
partList.push(line);
|
||||
});
|
||||
|
||||
line += partList.join(';');
|
||||
|
||||
list.push(line);
|
||||
}
|
||||
|
||||
list.forEach(line => {
|
||||
if (model.get('isHtml')) {
|
||||
prepending += '<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
63
client/src/exceptions.js
Normal 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
458
client/src/field-manager.js
Normal 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
30
client/src/global.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
46
client/src/handlers/api-user/open-api-spec-action.js
Normal file
46
client/src/handlers/api-user/open-api-spec-action.js
Normal 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');
|
||||
}
|
||||
}
|
||||
67
client/src/handlers/create-related.ts
Normal file
67
client/src/handlers/create-related.ts
Normal 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;
|
||||
65
client/src/handlers/create-related/set-parent.js
Normal file
65
client/src/handlers/create-related/set-parent.js
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
44
client/src/handlers/currency-record/menu-action-settings.js
Normal file
44
client/src/handlers/currency-record/menu-action-settings.js
Normal 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;
|
||||
}
|
||||
}
|
||||
45
client/src/handlers/currency-record/record-detail.js
Normal file
45
client/src/handlers/currency-record/record-detail.js
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
54
client/src/handlers/email-account/reset-fetch-data-action.js
Normal file
54
client/src/handlers/email-account/reset-fetch-data-action.js
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
126
client/src/handlers/email-filter.js
Normal file
126
client/src/handlers/email-filter.js
Normal 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;
|
||||
|
||||
54
client/src/handlers/email/list-actions.js
Normal file
54
client/src/handlers/email/list-actions.js
Normal 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;
|
||||
53
client/src/handlers/email/select-user.js
Normal file
53
client/src/handlers/email/select-user.js
Normal 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};
|
||||
}
|
||||
}
|
||||
51
client/src/handlers/import.js
Normal file
51
client/src/handlers/import.js
Normal 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;
|
||||
45
client/src/handlers/lead-capture/pipeline-select-handler.js
Normal file
45
client/src/handlers/lead-capture/pipeline-select-handler.js
Normal 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'],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
53
client/src/handlers/lead-capture/record-detail-view-setup.js
Normal file
53
client/src/handlers/lead-capture/record-detail-view-setup.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
70
client/src/handlers/login.js
Normal file
70
client/src/handlers/login.js
Normal 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;
|
||||
240
client/src/handlers/login/oidc.js
Normal file
240
client/src/handlers/login/oidc.js
Normal 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;
|
||||
163
client/src/handlers/map/google-maps-renderer.js
Normal file
163
client/src/handlers/map/google-maps-renderer.js
Normal 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;
|
||||
|
||||
63
client/src/handlers/map/renderer.js
Normal file
63
client/src/handlers/map/renderer.js
Normal 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;
|
||||
55
client/src/handlers/model/defaults-preparator.js
Normal file
55
client/src/handlers/model/defaults-preparator.js
Normal 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;
|
||||
48
client/src/handlers/navbar-menu.js
Normal file
48
client/src/handlers/navbar-menu.js
Normal 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;
|
||||
55
client/src/handlers/note/record-detail-setup.js
Normal file
55
client/src/handlers/note/record-detail-setup.js
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
58
client/src/handlers/o-auth-access-token/revoke-row-action.ts
Normal file
58
client/src/handlers/o-auth-access-token/revoke-row-action.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
92
client/src/handlers/pipeline-stage/move-row-action.js
Normal file
92
client/src/handlers/pipeline-stage/move-row-action.js
Normal 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();
|
||||
}
|
||||
}
|
||||
80
client/src/handlers/pipeline/move-row-action.js
Normal file
80
client/src/handlers/pipeline/move-row-action.js
Normal 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();
|
||||
}
|
||||
}
|
||||
147
client/src/handlers/record/lock-action.js
Normal file
147
client/src/handlers/record/lock-action.js
Normal 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;
|
||||
}
|
||||
}
|
||||
145
client/src/handlers/record/lock-mass-action.js
Normal file
145
client/src/handlers/record/lock-mass-action.js
Normal 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;
|
||||
}
|
||||
}
|
||||
163
client/src/handlers/record/lock-view-setup.js
Normal file
163
client/src/handlers/record/lock-view-setup.js
Normal 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;
|
||||
}
|
||||
}
|
||||
145
client/src/handlers/record/pipeline-view-setup.js
Normal file
145
client/src/handlers/record/pipeline-view-setup.js
Normal 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');
|
||||
}
|
||||
}
|
||||
72
client/src/handlers/record/view-audit-log.js
Normal file
72
client/src/handlers/record/view-audit-log.js
Normal 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
Reference in New Issue
Block a user