From e571a63a305fdf9914481ef3d0e28e03bf4658f2 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 22 Aug 2024 17:25:38 +0300 Subject: [PATCH] change user position action --- application/Espo/Entities/Team.php | 8 ++ .../Espo/Resources/i18n/en_US/User.json | 3 + .../Resources/metadata/clientDefs/Team.json | 13 +- .../Resources/metadata/clientDefs/User.json | 7 ++ application/Espo/Resources/routes.json | 5 + .../Tools/User/Api/PutTeamUserPosition.php | 65 ++++++++++ .../user/change-team-position-row-action.js | 95 ++++++++++++++ .../src/views/user/modals/select-position.js | 116 ++++++++++++++++++ 8 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 application/Espo/Tools/User/Api/PutTeamUserPosition.php create mode 100644 client/src/handlers/user/change-team-position-row-action.js create mode 100644 client/src/views/user/modals/select-position.js diff --git a/application/Espo/Entities/Team.php b/application/Espo/Entities/Team.php index b27254ee30..45c814d29c 100644 --- a/application/Espo/Entities/Team.php +++ b/application/Espo/Entities/Team.php @@ -49,4 +49,12 @@ class Team extends \Espo\Core\ORM\Entity /** @var ?Link */ return $this->getValueObject('layoutSet'); } + + /** + * @return string[] + */ + public function getPositionList(): array + { + return $this->get('positionList') ?? []; + } } diff --git a/application/Espo/Resources/i18n/en_US/User.json b/application/Espo/Resources/i18n/en_US/User.json index 87112d58fa..91e9bbbeb0 100644 --- a/application/Espo/Resources/i18n/en_US/User.json +++ b/application/Espo/Resources/i18n/en_US/User.json @@ -166,5 +166,8 @@ "active": "Active", "activePortal": "Portal Active", "activeApi": "API Active" + }, + "actions": { + "changePosition": "Change Position" } } diff --git a/application/Espo/Resources/metadata/clientDefs/Team.json b/application/Espo/Resources/metadata/clientDefs/Team.json index 7949e3c552..2d39572919 100644 --- a/application/Espo/Resources/metadata/clientDefs/Team.json +++ b/application/Espo/Resources/metadata/clientDefs/Team.json @@ -11,11 +11,18 @@ }, "relationshipPanels": { "users": { - "create": false, - "rowActionsView": "views/record/row-actions/relationship-unlink-only", + "createDisabled": true, + "editDisabled": true, + "removeDisabled": true, "layout": "listForTeam", "selectPrimaryFilterName": "active", - "filterList": ["all", "active"] + "filterList": ["all", "active"], + "rowActionList": [ + "changeTeamPosition" + ], + "selectMandatoryAttributeList": [ + "teamRole" + ] } }, "recordViews": { diff --git a/application/Espo/Resources/metadata/clientDefs/User.json b/application/Espo/Resources/metadata/clientDefs/User.json index 6688b13c09..312bf11f97 100644 --- a/application/Espo/Resources/metadata/clientDefs/User.json +++ b/application/Espo/Resources/metadata/clientDefs/User.json @@ -18,6 +18,13 @@ "detail": "views/user/modals/detail", "massUpdate": "views/user/modals/mass-update" }, + "rowActionDefs": { + "changeTeamPosition": { + "labelTranslation": "User.actions.changePosition", + "handler": "handlers/user/change-team-position-row-action", + "groupIndex": 3 + } + }, "defaultSidePanel": { "detail": { "name": "default", diff --git a/application/Espo/Resources/routes.json b/application/Espo/Resources/routes.json index f51a3c1c53..e573396dde 100644 --- a/application/Espo/Resources/routes.json +++ b/application/Espo/Resources/routes.json @@ -436,6 +436,11 @@ "actionClassName": "Espo\\Tools\\UserSecurity\\Api\\PostChangePasswordByRequest", "noAuth": true }, + { + "route": "/Team/:id/userPosition", + "method": "put", + "actionClassName": "Espo\\Tools\\User\\Api\\PutTeamUserPosition" + }, { "route": "/Oidc/authorizationData", "method": "get", diff --git a/application/Espo/Tools/User/Api/PutTeamUserPosition.php b/application/Espo/Tools/User/Api/PutTeamUserPosition.php new file mode 100644 index 0000000000..e05e58b4e9 --- /dev/null +++ b/application/Espo/Tools/User/Api/PutTeamUserPosition.php @@ -0,0 +1,65 @@ +user->isAdmin()) { + throw new Forbidden(); + } + + $teamId = $request->getRouteParam('id') ?? throw new BadRequest(); + $userId = $request->getParsedBody()->id; + $position = $request->getParsedBody()->position; + + if (!is_string($userId) || !$userId) { + throw new BadRequest("Bad userId."); + } + + if (!is_string($position) && $position !== null) { + throw new BadRequest("Bad position."); + } + + $user = $this->entityProvider->getByClass(User::class, $userId); + $team = $this->entityProvider->getByClass(Team::class, $teamId); + + if (!$this->acl->checkEntityEdit($user)) { + throw new Forbidden(); + } + + if ($position !== null && !in_array($position, $team->getPositionList())) { + throw new BadRequest("Not allowed position."); + } + + $this->entityManager + ->getRelation($team, 'users') + ->updateColumns($user, ['role' => $position]); + + return ResponseComposer::json(true); + } +} diff --git a/client/src/handlers/user/change-team-position-row-action.js b/client/src/handlers/user/change-team-position-row-action.js new file mode 100644 index 0000000000..b2b36aef40 --- /dev/null +++ b/client/src/handlers/user/change-team-position-row-action.js @@ -0,0 +1,95 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import RowActionHandler from 'handlers/row-action'; +import UserSelectPositionModalView from 'views/user/modals/select-position'; + +// noinspection JSUnusedGlobalSymbols +export default class ChangeUserTeamPositionRowActionHandler extends RowActionHandler { + + async process(model, action) { + if (!model.collection || !model.collection.parentModel) { + console.error(`Team model cannot be obtained.`); + + return; + } + + const team = model.collection.parentModel; + + /** @type {string[]} */ + const positionList = team.attributes.positionList || []; + const position = model.attributes.teamRole; + + const view = new UserSelectPositionModalView({ + position: position, + positionList: positionList, + name: model.attributes.name, + onApply: position => { + this.savePosition(team.id, model, position); + }, + }); + + await this.view.assignView('dialog', view); + await view.render(); + } + + isAvailable(model, action) { + if (!model.collection || !model.collection.parentModel) { + return false; + } + + if (!this.view.getAcl().checkModel(model, 'edit')) { + return false; + } + + if (!this.view.getUser().isAdmin()) { + return false; + } + + return true; + } + + /** + * @private + * @param {string} teamId + * @param {import('model').default} model + * @param {string|null} position + */ + async savePosition(teamId, model, position) { + Espo.Ui.notify(' ... '); + + await Espo.Ajax.putRequest(`Team/${teamId}/userPosition`, { + id: model.id, + position: position, + }); + + model.setMultiple({teamRole: position}); + + Espo.Ui.success(this.view.translate('Saved')); + } +} diff --git a/client/src/views/user/modals/select-position.js b/client/src/views/user/modals/select-position.js new file mode 100644 index 0000000000..6c33f3502d --- /dev/null +++ b/client/src/views/user/modals/select-position.js @@ -0,0 +1,116 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import ModalView from 'views/modal'; +import Model from 'model'; +import EditForModalRecordView from 'views/record/edit-for-modal'; +import EnumFieldView from 'views/fields/enum'; + +export default class UserSelectPositionModalView extends ModalView { + + templateContent = '
{{{record}}}
' + + className = 'dialog dialog-record' + + shortcutKeys = { + 'Control+Enter': 'apply', + } + + /** + * @param {{ + * positionList: string[], + * position: string|null, + * name: string, + * onApply: function(string|null), + * }} options + */ + constructor(options) { + super(options); + + /** @private */ + this.props = options; + } + + setup() { + this.headerText = + this.translate('changePosition', 'actions', 'User') + ' · ' + + this.props.name; + + this.buttonList = [ + { + name: 'save', + label: 'Save', + style: 'primary', + onClick: () => this.apply(), + }, + { + name: 'cancel', + label: 'Cancel', + } + ]; + + this.model = new Model(); + this.model.setMultiple({position: this.props.position}); + + this.recordView = new EditForModalRecordView({ + model: this.model, + detailLayout: [ + { + rows: [ + [ + { + view: new EnumFieldView({ + name: 'position', + params: { + options: ['', ...this.props.positionList], + }, + labelText: this.translate('teamRole', 'fields', 'User'), + }), + }, + false + ] + ] + } + ] + }); + + this.assignView('record', this.recordView, '.record'); + } + + /** + * @private + */ + apply() { + if (this.recordView.validate()) { + return; + } + + this.props.onApply(this.model.attributes.position); + this.close(); + } +}