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 2s
Test Integration on MySQL / Test on PHP 8.5 (push) Failing after 0s
Test Integration on PostgreSQL / Test on PHP 8.4 fix (push) Failing after 1s
Test Integration on PostgreSQL / Test on PHP 8.4 master (push) Failing after 1s

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

View File

@@ -0,0 +1,147 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import ActionHandler from 'action-handler';
import {inject} from 'di';
import AclManager from 'acl-manager';
import Metadata from 'metadata';
// noinspection JSUnusedGlobalSymbols
export default class LockActionHandler extends ActionHandler {
/**
* @type {AclManager}
*/
@inject(AclManager)
acl
/**
* @type {Metadata}
*/
@inject(Metadata)
metadata
async actionLock() {
const model = this.view.model;
Espo.Ui.notifyWait();
let attributes;
try {
attributes = await Espo.Ajax.postRequest('Action', {
action: 'lock',
entityType: model.entityType,
id: model.id,
});
} catch (e) {
return;
}
model.setMultiple(attributes, {sync: true});
model.trigger('sync', this.model, null, {});
Espo.Ui.success(this.view.translate('locked', 'messages'));
}
async actionUnlock() {
const model = this.view.model;
Espo.Ui.notifyWait();
let attributes;
try {
attributes = await Espo.Ajax.postRequest('Action', {
action: 'unlock',
entityType: model.entityType,
id: model.id,
});
} catch (e) {
return;
}
model.setMultiple(attributes, {sync: true});
model.trigger('sync', this.model, null, {});
Espo.Ui.success(this.view.translate('unlocked', 'messages'));
}
// noinspection JSUnusedGlobalSymbols
/**
* @return {boolean}
*/
canBeLocked() {
const model = this.view.model;
if (!this.isEnabled(model.entityType)) {
return false;
}
if (this.acl.getPermissionLevel('lock') !== 'yes') {
return false;
}
if (model.attributes.isLocked) {
return false;
}
return true;
}
// noinspection JSUnusedGlobalSymbols
/**
* @return {boolean}
*/
canBeUnlocked() {
const model = this.view.model;
if (!this.isEnabled(model.entityType)) {
return false;
}
if (this.acl.getPermissionLevel('lock') !== 'yes') {
return false;
}
if (!model.attributes.isLocked) {
return false;
}
return true;
}
/**
* @private
* @param {string} entityType
* @return {boolean}
*/
isEnabled(entityType) {
return this.metadata.get(`scopes.${entityType}.lockable`) === true;
}
}

View File

@@ -0,0 +1,145 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import ActionHandler from 'action-handler';
import {inject} from 'di';
import AclManager from 'acl-manager';
import Language from 'language';
import MassActionHelper from 'helpers/mass-action';
import Metadata from 'metadata';
// noinspection JSUnusedGlobalSymbols
export default class LockMassActionHandler extends ActionHandler {
/**
* @type {AclManager}
*/
@inject(AclManager)
acl
/**
* @type {Language}
*/
@inject(Language)
language
/**
* @type {Metadata}
*/
@inject(Metadata)
metadata
// noinspection JSUnusedGlobalSymbols
async actionLock() {
const msg = this.language.translate('confirmMassLock', 'messages');
await this.view.confirm(msg);
await this.process('lock');
}
// noinspection JSUnusedGlobalSymbols
async actionUnlock() {
const msg = this.language.translate('confirmMassUnlock', 'messages');
await this.view.confirm(msg);
await this.process('unlock');
}
/**
* @private
* @param {string} action
*/
async process(action) {
const helper = new MassActionHelper(this.view);
const params = this.view.getMassActionSelectionPostData();
const idle = !!params.searchParams && helper.checkIsIdle(this.view.collection.total);
const onDone = count => {
const labelKey = action === 'lock' ? 'massLockDone': 'massUnlockDone';
const msg = this.view.translate(labelKey, 'messages')
.replace('{count}', count.toString());
Espo.Ui.success(msg);
};
Espo.Ui.notifyWait();
const result = await Espo.Ajax.postRequest('MassAction', {
entityType: this.view.entityType,
action: action,
params: params,
idle: idle,
});
if (result.id) {
const view = await helper.process(result.id, action)
this.view.listenToOnce(view, 'close:success', result => onDone(result.count));
return;
}
onDone(result.count);
}
// noinspection JSUnusedGlobalSymbols
initLock() {
if (
!this.view.collection ||
!this.isEnabled(this.view.collection.entityType) ||
this.acl.getPermissionLevel('massUpdate') !== 'yes' ||
this.acl.getPermissionLevel('lock') !== 'yes'
) {
this.view.removeMassAction('lock');
}
}
// noinspection JSUnusedGlobalSymbols
initUnlock() {
if (
!this.view.collection ||
!this.isEnabled(this.view.collection.entityType) ||
this.acl.getPermissionLevel('massUpdate') !== 'yes' ||
this.acl.getPermissionLevel('lock') !== 'yes'
) {
this.view.removeMassAction('unlock');
}
}
/**
* @private
* @param {string} entityType
* @return {boolean}
*/
isEnabled(entityType) {
return this.metadata.get(`scopes.${entityType}.lockable`) === true;
}
}

View File

@@ -0,0 +1,163 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import {inject} from 'di';
import Metadata from 'metadata';
import AssignmentHelper from 'helpers/assignment';
// noinspection JSUnusedGlobalSymbols
export default class LockViewSetup {
/**
* @private
* @type {Metadata}
*/
@inject(Metadata)
metadata
/**
* @private
* @type {import('model').default}
*/
model
/**
* @private
* @type {import('views/record/detail').default}
*/
view
ignoreFieldList = [
'isLocked',
'modifiedAt',
'modifiedBy',
'streamUpdatedAt',
]
/**
* @param {import('views/record/detail').default} view
*/
constructor(view) {
this.view = view;
this.model = view.model;
}
process() {
const entityType = this.model.entityType;
if (this.metadata.get(`scopes.${entityType}.lockable`) !== true) {
return;
}
let wasLocked = false;
const lockedMap = {};
const ignoreFieldList = this.getIgnoreFieldList();
const fieldsDefs = /** @type {Record.<string, Record>} */
this.metadata.get(`entityDefs.${entityType}.fields`) ?? {};
const lockableFields = Object.keys(fieldsDefs)
.filter(field => {
if (ignoreFieldList.includes(field)) {
return false;
}
const defs = fieldsDefs[field];
return !defs.notLockable &&
!defs.readOnly &&
!defs.readOnlyAfterCreate;
});
const controlLocked = () => {
const isLocked = this.model.attributes.isLocked;
if (!isLocked && !wasLocked) {
return;
}
if (isLocked) {
wasLocked = true;
}
lockableFields.forEach(field => {
if (isLocked) {
if (
this.view.recordHelper &&
this.view.recordHelper.getFieldStateParam(field, 'readOnly')
) {
return;
}
lockedMap[field] = true;
this.view.setFieldReadOnly(field);
return;
}
if (!lockedMap[field]) {
return;
}
delete lockedMap[field];
this.view.setFieldNotReadOnly(field);
});
}
controlLocked();
this.view.listenTo(this.model, 'change:isLocked', () => controlLocked());
}
/**
* @private
* @return {string[]}
*/
getIgnoreFieldList() {
const entityType = this.model.entityType;
const ignoreFieldList = [...this.ignoreFieldList];
const helper = new AssignmentHelper;
if (helper.hasCollaboratorsField(entityType)) {
if (helper.hasAssignedUsersField(entityType)) {
if (this.model.getFieldParam('assignedUsers', 'notLockable')) {
ignoreFieldList.push('collaborators');
}
} else if (helper.hasAssignedUserField(entityType)) {
if (this.model.getFieldParam('assignedUser', 'notLockable')) {
ignoreFieldList.push('collaborators');
}
}
}
return ignoreFieldList;
}
}

View File

@@ -0,0 +1,145 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import {inject} from 'di';
import Metadata from 'metadata';
import AppParams from 'app-params';
import PipelinesHelper from 'helpers/misc/pipelines';
// noinspection JSUnusedGlobalSymbols
export default class PipelineViewSetupHandler {
/**
* @private
* @type {Metadata}
*/
@inject(Metadata)
metadata
/**
* @private
* @type {import('model').default}
*/
model
/**
* @private
* @type {import('views/record/detail').default}
*/
view
/**
* @private
* @type {AppParams}
*/
@inject(AppParams)
appParams
/**
* @private
* @type {{id: string, stages: {id: string, name: string, style: string|null}[]}[]}
*/
pipelines
/**
* @param {import('views/record/detail').default} view
*/
constructor(view) {
this.view = view;
this.model = view.model;
}
process() {
const entityType = this.model.entityType;
if (this.metadata.get(`scopes.${entityType}.pipelines`) !== true) {
return;
}
this.pipelines = new PipelinesHelper().get(entityType);
this.model.onChange({
owner: this.view,
attributes: ['pipelineId'],
callback: o => {
if (!o.ui) {
return;
}
setTimeout(() => this.updateStage(), 1);
},
});
}
/**
* @private
*/
updateStage() {
const pipelineId = this.model.attributes.pipelineId;
if (!pipelineId) {
this.setStageNull();
return;
}
const pipeline = this.pipelines.find(it => it.id === pipelineId);
if (!pipeline) {
this.setStageNull();
return;
}
const stage = pipeline.stages[0];
if (!stage) {
this.setStageNull();
return;
}
this.model.setMultiple({
pipelineStageId: stage.id,
pipelineStageName: stage.name,
});
this.model.trigger('pipeline-changed');
}
/**
* @private
*/
setStageNull() {
this.model.setMultiple({
pipelineStageId: null,
pipelineStageName: null,
});
this.model.trigger('pipeline-changed');
}
}

View File

@@ -0,0 +1,72 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM – Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import StreamViewAuditLogModalView from 'views/stream/modals/view-audit-log';
class ViewAuditLogHandler {
constructor(/** import('views/record/detail').default */view) {
this.view = view;
this.metadata = /** @type {module:metadata} */view.getMetadata();
this.entityType = this.view.entityType;
this.model = /** @type {module:model} */this.view.model;
this.hasAudited =
this.metadata.get(`scopes.${this.entityType}.statusField`) ||
this.model.getFieldList().find(field => this.model.getFieldParam(field, 'audited')) !== undefined;
if (this.entityType === 'User' && !this.view.getUser().isAdmin()) {
this.hasAudited = false;
}
if (this.view.getUser().isPortal()) {
this.hasAudited = false;
}
if (this.view.getAcl().getPermissionLevel('audit') !== 'yes') {
this.hasAudited = false;
}
}
isAvailable() {
return this.hasAudited;
}
show() {
const view = new StreamViewAuditLogModalView({model: this.model});
this.view.assignView('dialog', view)
.then(() => {
view.render();
})
}
}
// noinspection JSUnusedGlobalSymbols
export default ViewAuditLogHandler;

View File

@@ -0,0 +1,120 @@
/************************************************************************
* 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 RelatedListModalView from 'views/modals/related-list';
export default class {
toShow
constructor(/** import('views/record/detail').default */view) {
this.view = view;
this.metadata = /** @type {module:metadata} */view.getMetadata();
this.entityType = this.view.entityType;
this.model = this.view.model;
const level = this.view.getAcl().getPermissionLevel('user');
this.toShow = (level === 'all' || level === 'team') &&
(
this.metadata.get(`scopes.${this.entityType}.object`) ||
this.metadata.get(`scopes.${this.entityType}.acl`)
)
this.view.getAcl().checkScope('User');
}
isAvailable() {
return this.toShow;
}
async show() {
const actionList = this.getActionList();
/** @type {Record[]} */
const listLayout = [
{
name: 'name',
link: true,
view: 'views/user/fields/name',
},
];
//const width = Math.round((100.0 - 40) / (actionList.length + 1));
actionList.forEach(action => {
listLayout.push({
name: 'recordAccessLevel' + action,
customLabel: this.view.translate(action, 'recordActions'),
view: 'views/user/fields/record-access-level',
notSortable: true,
width: 16,
});
});
const view = new RelatedListModalView({
model: this.model,
link: 'usersAccess',
entityType: 'User',
title: this.view.translate('View User Access'),
url: `${this.entityType}/${this.model.id}/usersAccess`,
createDisabled: true,
selectDisabled: true,
massActionsDisabled: true,
maxSize: this.view.getConfig().get('recordsPerPageSmall'),
rowActionsView: null,
listLayout: listLayout,
filter: 'active',
});
await this.view.assignView('dialog', view);
await view.render();
}
/**
* @private
* @return {string[]}
*/
getActionList() {
/** @type {string[]} */
let actionList = this.metadata.get(`scopes.${this.entityType}.aclActionList`);
if (!actionList) {
actionList = [
'read',
'edit',
'delete',
];
if (this.metadata.get(`scopes.${this.entityType}.stream`)) {
actionList.push('stream');
}
}
return actionList.filter(it => it !== 'create');
}
}