This commit is contained in:
Yurii
2026-02-26 13:59:14 +02:00
parent feac638722
commit dcd3ce66ce
65 changed files with 2298 additions and 52 deletions
+4
View File
@@ -277,6 +277,10 @@ class AclManager {
return this.checkScope(subject, action, precise);
}
if (!subject) {
throw new Error("No model.");
}
return this.checkModel(subject, action, precise);
}
+5
View File
@@ -1268,6 +1268,11 @@ class App {
break;
case 409:
this._processErrorAlert(xhr, 'Conflict');
break;
case 404:
// noinspection JSUnresolvedReference
if (options.main) {
+147
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;
}
}
@@ -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;
}
}
@@ -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;
}
}
+75
View File
@@ -0,0 +1,75 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
import {inject} from 'di';
import Metadata from 'metadata';
/**
* @since 9.4.0
*/
export default class AssignmentHelper {
/**
* @private
* @type {Metadata}
*/
@inject(Metadata)
metadata
/**
* @param {string} entityType
* @return {boolean}
*/
hasAssignedUserField(entityType) {
return this.metadata.get(`entityDefs.${entityType}.fields.assignedUser.type`) === 'link' &&
!this.metadata.get(`entityDefs.${entityType}.fields.assignedUser.disabled`) &&
this.metadata.get(`entityDefs.${entityType}.links.assignedUser.entity`) === 'User';
}
/**
* @param {string} entityType
* @return {boolean}
*/
hasAssignedUsersField(entityType) {
return this.metadata.get(`entityDefs.${entityType}.fields.assignedUsers.type`) === 'linkMultiple' &&
this.metadata.get(`entityDefs.${entityType}.links.assignedUsers.entity`) === 'User';
}
/**
* @param {string} entityType
* @return {boolean}
*/
hasCollaboratorsField(entityType) {
if (!this.metadata.get(`scopes.${entityType}.collaborators`)) {
return false;
}
return this.metadata.get(`entityDefs.${entityType}.fields.collaborators.type`) === 'linkMultiple' &&
this.metadata.get(`entityDefs.${entityType}.links.collaborators.entity`) === 'User';
}
}
@@ -87,6 +87,12 @@ class FieldManagerEditView extends View {
*/
paramDataList
/**
* @private
* @type {boolean}
*/
isEntityTypeLockable
data() {
return {
scope: this.scope,
@@ -146,6 +152,8 @@ class FieldManagerEditView extends View {
this.entityTypeIsCustom = !!this.getMetadata().get(['scopes', this.scope, 'isCustom']);
this.isEntityTypeLockable = this.getMetadata().get(`scopes.${this.scope}.lockable`) === true;
this.globalRestriction = {};
if (!this.isNew) {
@@ -312,6 +320,16 @@ class FieldManagerEditView extends View {
});
}
if (
this.isEntityTypeLockable &&
!this.globalRestriction.readOnly
) {
this.paramList.push({
name: 'notLockable',
type: 'bool',
});
}
if (this.hasTooltipText) {
this.paramList.push({
name: 'tooltipText',
@@ -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 BoolFieldView from 'views/fields/bool';
export default class IsLockedFieldView extends BoolFieldView {
//language=Handlebars
detailTemplateContent = `
{{~#if valueIsSet~}}
{{~#if value~}}
<span class="fas fa-lock text-soft"></span>
{{~else~}}
<input
class="form-checkbox"
type="checkbox" {{#if value}} checked {{/if}}
disabled
>
{{~/if~}}
{{~else~}}
<span class="loading-value"></span>
{{~/if~}}
`
}
+2 -2
View File
@@ -2330,7 +2330,7 @@ class ListRecordView extends View {
/** @private */
setupMassActions() {
if (this.massActionsDisabled) {
if (this.massActionsDisabled || !this.checkboxes) {
this.massActionList = [];
this.checkAllResultMassActionList = [];
this.massActionDefs = {};
@@ -2391,7 +2391,7 @@ class ListRecordView extends View {
if (
!Espo.Utils.checkActionAvailability(this.getHelper(), defs) ||
!Espo.Utils.checkActionAccess(this.getAcl(), this.entityType, defs)
this.entityType && !Espo.Utils.checkActionAccess(this.getAcl(), this.entityType, defs)
) {
return;
}
@@ -45,6 +45,12 @@ class DefaultSidePanelView extends SidePanelView {
*/
complexModifiedDisabled
/**
* @protected
* @type {boolean}
*/
hasIsLocked
data() {
const data = super.data();
@@ -74,12 +80,24 @@ class DefaultSidePanelView extends SidePanelView {
allFieldList.includes('modifiedAt') ||
allFieldList.includes('modifiedBy');
this.hasIsLocked = this.getMetadata().get(`scopes.${this.model.entityType}.lockable`) === true;
super.setup();
}
setupFields() {
super.setupFields();
if (this.hasIsLocked) {
this.fieldList.push({
name: 'isLocked',
view: 'views/global/fields/is-locked',
});
this.controlIsLockedField();
this.listenTo(this.model, 'change:isLocked', () => this.controlIsLockedField());
}
if (!this.complexCreatedDisabled) {
if (this.hasComplexCreated) {
this.fieldList.push({
@@ -194,6 +212,17 @@ class DefaultSidePanelView extends SidePanelView {
this.recordViewObject.hideField('followers');
}
/**
* @private
*/
controlIsLockedField() {
if (this.model.attributes.isLocked) {
this.recordViewObject.showField('isLocked');
} else {
this.recordViewObject.hideField('isLocked');
}
}
}
export default DefaultSidePanelView;