From e1ffa65d8b7c8f06e248e7980ab184d41ac97d5e Mon Sep 17 00:00:00 2001 From: Stephen Abello Date: Wed, 18 Jan 2023 13:35:48 +0100 Subject: [PATCH] =?UTF-8?q?N=C2=B03136=20-=20Add=20creation=20and=20modifi?= =?UTF-8?q?cation=20of=20n-n=20objects=20in=20object=20details=20(#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rebase onto develop * Use exit condition instead of englobing condition * Add informative modals that can be called from modal toolbox * Refactor "apply_modify" and "apply_new" into own controller, handle ajax requests with a json response and handle these responses in linkset creation/edition * Fix merge issues * Remove inverted condition * Move linkset create button to a better place, still needs to fix duplicate "New" button caused by a refactor * Handle "Cancel" button in modals * Do not display relations when editing an object in a modal * More elegant way to add "New" button to relations lists * Factorize vertical highlights in alerts and modal in a single mixin * Replace button name with dict entry code * Change route name to snake case * More elegant way to add "Create in modal" button to relations lists * Replace triple if with in_array * Move listener to body * Rename variable to match boolean rules * Rename event * Rename extra param * Add phpdoc * Revert changes * Check indirect linkset rights before allowing creation in modal --- application/cmdbabstract.class.inc.php | 19 +- application/displayblock.class.inc.php | 27 +- css/backoffice/components/_alert.scss | 14 +- css/backoffice/components/_modal.scss | 32 +- .../layout/tab-container/_tab-container.scss | 2 +- css/backoffice/utils/mixins/_all.scss | 3 +- css/backoffice/utils/mixins/_highlight.scss | 17 + .../links/en.dictionary.itop.links.php | 3 + .../modal/en.dictionary.itop.modal.php | 4 + js/links/link_set_worker.js | 75 ++- js/pages/backoffice/toolbox.js | 52 ++- js/utils.js | 14 + pages/UI.php | 298 +----------- .../UI/Base/Component/DataTable/DataTable.php | 23 + .../DataTable/DataTableUIBlockFactory.php | 9 + .../UI/Links/AbstractBlockLinksViewTable.php | 19 +- .../Indirect/BlockIndirectLinksViewTable.php | 18 +- sources/Application/WebPage/iTopWebPage.php | 1 + .../Base/Layout/ObjectController.php | 429 +++++++++++++++++- .../Controller/Links/LinksetController.php | 98 ++++ .../components/datatable/layout.ready.js.twig | 6 + 21 files changed, 835 insertions(+), 328 deletions(-) create mode 100644 css/backoffice/utils/mixins/_highlight.scss diff --git a/application/cmdbabstract.class.inc.php b/application/cmdbabstract.class.inc.php index bea76227d..45bac9ab4 100644 --- a/application/cmdbabstract.class.inc.php +++ b/application/cmdbabstract.class.inc.php @@ -2775,8 +2775,13 @@ JS $oPage->AddUiBlock($oContentBlock); $oForm = new Form("form_{$this->m_iFormId}"); - $oForm->SetAction($sFormAction) - ->SetOnSubmitJsCode("return OnSubmit('form_{$this->m_iFormId}');"); + $oForm->SetAction($sFormAction); + $sOnSubmitForm = "let bOnSubmitForm = OnSubmit('form_{$this->m_iFormId}');"; + if (isset($aExtraParams['js_handlers']['form_on_submit'])) { + $oForm->SetOnSubmitJsCode($sOnSubmitForm.$aExtraParams['js_handlers']['form_on_submit']); + } else { + $oForm->SetOnSubmitJsCode($sOnSubmitForm."return bOnSubmitForm;"); + } $oContentBlock->AddSubBlock($oForm); if ($this->GetDisplayMode() === static::ENUM_DISPLAY_MODE_EDIT) { @@ -2959,7 +2964,15 @@ EOF // Hook the cancel button via jQuery so that it can be unhooked easily as well if needed $sDefaultUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=search_form&class='.$sClass.'&'.$oAppContext->GetForLink(); - $oPage->add_ready_script("$('#form_{$this->m_iFormId} button.cancel').on('click', function() { BackToDetails('$sClass', $iKey, '$sDefaultUrl', $sJSToken)} );"); + + $sCancelButtonOnClickScript = "let fOnClick{$this->m_iFormId}CancelButton = "; + if(isset($aExtraParams['js_handlers']['cancel_button_on_click'])){ + $sCancelButtonOnClickScript .= $aExtraParams['js_handlers']['cancel_button_on_click']; + } else { + $sCancelButtonOnClickScript .= "function() { BackToDetails('$sClass', $iKey, '$sDefaultUrl', $sJSToken)};"; + } + $sCancelButtonOnClickScript .= "$('#form_{$this->m_iFormId} button.cancel').on('click', fOnClick{$this->m_iFormId}CancelButton);"; + $oPage->add_ready_script($sCancelButtonOnClickScript); $iFieldsCount = count($aFieldsMap); $sJsonFieldsMap = json_encode($aFieldsMap); diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 361cbb10b..46e65c36c 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -1760,6 +1760,7 @@ class MenuBlock extends DisplayBlock $iSetCount = $oSet->Count(); /** @var string $sRefreshAction JS snippet to run when clicking on the refresh button of the menu */ $sRefreshAction = $aExtraParams['refresh_action'] ?? ''; + $bIsCreationInModalAllowed = isset($aExtraParams['creation_in_modal_is_allowed']) && $aExtraParams['creation_in_modal_is_allowed'] === true; /** @var array $aRegularActions Any action other than a transition */ $aRegularActions = []; @@ -1802,7 +1803,7 @@ class MenuBlock extends DisplayBlock // Any style actions // - Bulk actions on objects set if ($iSetCount > 1) { - if ($bIsCreationAllowed) { + if ($bIsCreationAllowed && !$bIsCreationInModalAllowed) { $this->AddNewObjectMenuAction($aRegularActions, $sClass, $sDefaultValuesAsUrlParams); } @@ -2216,11 +2217,31 @@ class MenuBlock extends DisplayBlock $oActionsToolbar->AddSubBlock($oActionButton); } - if ($this->m_sStyle == 'details') { - // - Search + // - Search + if ($this->m_sStyle === 'details') { $oActionButton = ButtonUIBlockFactory::MakeIconLink('fas fa-search', Dict::Format('UI:SearchFor_Class', MetaModel::GetName($sClass)), "{$sRootUrl}pages/UI.php?operation=search_form&do_search=0&class=$sClass{$sContext}", '', 'UI:SearchFor_Class'); $oActionButton->AddCSSClasses(['ibo-action-button', 'ibo-regular-action-button']); $oActionsToolbar->AddSubBlock($oActionButton); + } + + // - Creation in modal + if($bIsCreationInModalAllowed === true){ + $oAddLinkActionButton = ButtonUIBlockFactory::MakeIconAction( + 'fas fa-plus', + Dict::S('UI:Links:New:Button:Tooltip'), + 'UI:Links:New', + '', + false + ); + + // - If we are used in a Datatable, 'datatable_' will be prefixed to our $sId, so we do the same here + $sRealId = $sId; + if(in_array($this->m_sStyle, ['list', 'links', 'listInObject'])){ + $sRealId = 'datatable_' . $sId; + } + $oAddLinkActionButton->AddCSSClasses(['ibo-action-button', 'ibo-regular-action-button']) + ->SetOnClickJsCode("$('#$sRealId').trigger('open_creation_modal.object.itop');"); + $oActionsToolbar->AddSubBlock($oAddLinkActionButton); } // - Others diff --git a/css/backoffice/components/_alert.scss b/css/backoffice/components/_alert.scss index 719ec20b9..e55318de0 100644 --- a/css/backoffice/components/_alert.scss +++ b/css/backoffice/components/_alert.scss @@ -1,5 +1,5 @@ /* - * @copyright Copyright (C) 2010-2021 Combodo SARL + * @copyright Copyright (C) 2010-2023 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ @@ -9,9 +9,6 @@ $ibo-alert--padding-x: 20px !default; $ibo-alert--min-height: 30px !default; $ibo-alert--border-radius: $ibo-border-radius-300 !default; -$ibo-alert--title--highlight--width: 4px !default; -$ibo-alert--title--highlight--height: 100% !default; - $ibo-alert--body--margin-top: $ibo-spacing-200 !default; $ibo-alert-minimized--padding-y: 5px !default; @@ -68,14 +65,7 @@ $ibo-alert-colors: ( @extend %ibo-font-size-150; &::before { - display: block; - position: absolute; - top: 0; - left: 0; - content: ''; - - width: $ibo-alert--title--highlight--width; - height: $ibo-alert--title--highlight--height; + @include ibo-vertical-highlight; } .ibo-alert--title { diff --git a/css/backoffice/components/_modal.scss b/css/backoffice/components/_modal.scss index 0cb0f44de..2e4be2a75 100644 --- a/css/backoffice/components/_modal.scss +++ b/css/backoffice/components/_modal.scss @@ -1,11 +1,18 @@ /* - * @copyright Copyright (C) 2010-2021 Combodo SARL + * @copyright Copyright (C) 2010-2023 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ /* SCSS variables */ $ibo-modal-option--do-not-show-again--margin-top: $ibo-spacing-500 !default; +$ibo-modal--is-informative--min-width: $ibo-size-700 !default; +$ibo-modal--is-informative--min-height: $ibo-size-300 !default; +$ibo-modal--is-informative--is-error--highlight--background-color: $ibo-color-red-600 !default; +$ibo-modal--is-informative--is-warning--highlight--background-color: $ibo-color-orange-600 !default; +$ibo-modal--is-informative--is-information--highlight--background-color: $ibo-color-blue-600 !default; +$ibo-modal--is-informative--is-success--highlight--background-color: $ibo-color-green-600 !default; + // Modal Option - Do not show again .ibo-modal-option--do-not-show-again{ margin-top: $ibo-modal-option--do-not-show-again--margin-top; @@ -15,4 +22,27 @@ $ibo-modal-option--do-not-show-again--margin-top: $ibo-spacing-500 !default; display: inline-block; width: auto; } +} + +.ibo-modal.ibo-is-informative{ + display: flex; + align-items: center; + min-width: $ibo-modal--is-informative--min-width; + min-height: $ibo-modal--is-informative--min-height !important; // !important in order to overload jQueryUI CSS rule that's put directly on the element + + &::before { + @include ibo-vertical-highlight; + } + &.ibo-is-error::before { + background-color: $ibo-modal--is-informative--is-error--highlight--background-color; + } + &.ibo-is-warning::before { + background-color: $ibo-modal--is-informative--is-warning--highlight--background-color; + } + &.ibo-is-information::before { + background-color: $ibo-modal--is-informative--is-information--highlight--background-color; + } + &.ibo-is-success::before { + background-color: $ibo-modal--is-informative--is-success--highlight--background-color; + } } \ No newline at end of file diff --git a/css/backoffice/layout/tab-container/_tab-container.scss b/css/backoffice/layout/tab-container/_tab-container.scss index a4fd072c6..8873c0878 100644 --- a/css/backoffice/layout/tab-container/_tab-container.scss +++ b/css/backoffice/layout/tab-container/_tab-container.scss @@ -160,7 +160,7 @@ $ibo-tab-container--tab-container--last--min-height: 60vh !default; .ibo-tab-container--tab-container { min-height: $ibo-tab-container--tab-container--min-height; } - .ibo-tab-container--tab-container:last-child { + .ibo-tab-container--tab-container:last-child:not(:only-child) { min-height: $ibo-tab-container--tab-container--last--min-height; } } diff --git a/css/backoffice/utils/mixins/_all.scss b/css/backoffice/utils/mixins/_all.scss index df4932f68..bb4c6e405 100644 --- a/css/backoffice/utils/mixins/_all.scss +++ b/css/backoffice/utils/mixins/_all.scss @@ -1,5 +1,6 @@ /* - * @copyright Copyright (C) 2010-2021 Combodo SARL + * @copyright Copyright (C) 2010-2023 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ +@import "highlight"; \ No newline at end of file diff --git a/css/backoffice/utils/mixins/_highlight.scss b/css/backoffice/utils/mixins/_highlight.scss new file mode 100644 index 000000000..1201d82c6 --- /dev/null +++ b/css/backoffice/utils/mixins/_highlight.scss @@ -0,0 +1,17 @@ +/* + * @copyright Copyright (C) 2010-2023 Combodo SARL + * @license http://opensource.org/licenses/AGPL-3.0 + */ + +$ibo-vertical-highlight--width: $ibo-size-100; +$ibo-vertical-highlight--height: 100%; + +@mixin ibo-vertical-highlight { + display: block; + position: absolute; + top: 0; + left: 0; + content: ""; + width: $ibo-vertical-highlight--width; + height: $ibo-vertical-highlight--height; +} \ No newline at end of file diff --git a/dictionaries/ui/application/links/en.dictionary.itop.links.php b/dictionaries/ui/application/links/en.dictionary.itop.links.php index 115cbeb58..cf86b1fd2 100644 --- a/dictionaries/ui/application/links/en.dictionary.itop.links.php +++ b/dictionaries/ui/application/links/en.dictionary.itop.links.php @@ -25,4 +25,7 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Links:ActionRow:Delete' => 'Delete', 'UI:Links:ActionRow:Delete+' => 'Delete this object', 'UI:Links:ActionRow:Delete:Confirmation' => 'Do you really want to delete {item} from current object ?', + 'UI:Links:ActionRow:Modify:Modal:Title' => 'Modify a link', + 'UI:Links:New:Modal:Title' => 'Creation of a link', + 'UI:Links:New:Button:Tooltip' => 'Add a new link', )); \ No newline at end of file diff --git a/dictionaries/ui/components/modal/en.dictionary.itop.modal.php b/dictionaries/ui/components/modal/en.dictionary.itop.modal.php index 52f8a3cb2..88e9e23e5 100644 --- a/dictionaries/ui/components/modal/en.dictionary.itop.modal.php +++ b/dictionaries/ui/components/modal/en.dictionary.itop.modal.php @@ -19,4 +19,8 @@ Dict::Add('EN US', 'English', 'English', array( 'UI:Modal:Confirmation:DefaultTitle' => 'Confirmation', + 'UI:Modal:Informative:Title' => 'Informative Modal', + 'UI:Modal:InformativeError:Title' => 'Error', + 'UI:Modal:InformativeWarning:Title' => 'Warning', + 'UI:Modal:InformativeInformation:Title' => 'Information', )); \ No newline at end of file diff --git a/js/links/link_set_worker.js b/js/links/link_set_worker.js index 3e1cb144f..38d1c92e3 100644 --- a/js/links/link_set_worker.js +++ b/js/links/link_set_worker.js @@ -2,8 +2,10 @@ let LinkSetWorker = new function(){ // defines const ROUTER_BASE_URL = '../pages/ajax.render.php'; - const ROUTE_LINK_SET_DELETE_OBJECT = 'linkset.DeleteLinkedObject'; - const ROUTE_LINK_SET_DETACH_OBJECT = 'linkset.DetachLinkedObject'; + const ROUTE_LINK_SET_DELETE_OBJECT = 'linkset.delete_linked_object'; + const ROUTE_LINK_SET_DETACH_OBJECT = 'linkset.detach_linked_object'; + const ROUTE_LINK_SET_MODIFY_OBJECT = 'object.modify'; + const ROUTE_LINK_SET_CREATE_OBJECT = 'linkset.create_linked_object'; /** * CallAjaxDeleteLinkedObject. @@ -51,8 +53,75 @@ let LinkSetWorker = new function(){ }); }; + /** + * CallAjaxModifyLinkedObject. + * + * @param {string} sLinkedObjectClass + * @param {string} sLinkedObjectKey + * @param {string} sTableId + * @constructor + */ + const CallAjaxModifyLinkedObject = function(sLinkedObjectClass, sLinkedObjectKey, sTableId){ + let oTable = $('#datatable_' + sTableId); + let oTableSettingsDialog = $('#datatable_dlg_datatable_'+sTableId); + + let oOptions = { + title: Dict.S('UI:Links:ActionRow:Modify:Modal:Title'), + content: { + endpoint: `${ROUTER_BASE_URL}?route=${ROUTE_LINK_SET_MODIFY_OBJECT}`, + data: { + class: sLinkedObjectClass, + id: sLinkedObjectKey, + }, + }, + extra_options: { + callback_on_modal_close: function () { + oTableSettingsDialog.DataTableSettings('DoRefresh'); + $(this).find("form").remove(); + $(this).dialog('destroy'); + } + }, + } + CombodoModal.OpenModal(oOptions); + }; + + /** + * @param {string} sTableId + */ + const CallAjaxCreateLinkedObject = function(sTableId){ + let oTable = $('#datatable_' + sTableId); + let oTableSettingsDialog = $('#datatable_dlg_datatable_'+sTableId); + let sClass = oTable.closest('[data-role="ibo-block-links-table"]').attr('data-link-class'); + let sAttCode = oTable.closest('[data-role="ibo-block-links-table"]').attr('data-link-attcode'); + let sHostObjectClass = oTable.closest('[data-role="ibo-object-details"]').attr('data-object-class'); + let sHostObjectId = oTable.closest('[data-role="ibo-object-details"]').attr('data-object-id'); + + let oOptions = { + title: Dict.S('UI:Links:New:Modal:Title'), + content: { + endpoint: `${ROUTER_BASE_URL}?route=${ROUTE_LINK_SET_CREATE_OBJECT}`, + data: { + class: sClass, + att_code: sAttCode, + host_class: sHostObjectClass, + host_id: sHostObjectId + } + }, + extra_options: { + callback_on_modal_close: function () { + oTableSettingsDialog.DataTableSettings('DoRefresh'); + $(this).find("form").remove(); + $(this).dialog('destroy'); + } + }, + } + CombodoModal.OpenModal(oOptions); + }; + return { DeleteLinkedObject: CallAjaxDeleteLinkedObject, - DetachLinkedObject: CallAjaxDetachLinkedObject + DetachLinkedObject: CallAjaxDetachLinkedObject, + ModifyLinkedObject: CallAjaxModifyLinkedObject, + CreateLinkedObject: CallAjaxCreateLinkedObject } }; \ No newline at end of file diff --git a/js/pages/backoffice/toolbox.js b/js/pages/backoffice/toolbox.js index 5d0835632..40b2277ed 100644 --- a/js/pages/backoffice/toolbox.js +++ b/js/pages/backoffice/toolbox.js @@ -201,6 +201,7 @@ CombodoModal._InstantiateModal = function(oModalElem, oOptions) { width: 'auto', height: 'auto', modal: oOptions.extra_options.modal ?? true, + classes: oOptions.classes ?? {}, close: oOptions.extra_options.callback_on_modal_close, autoOpen: oOptions.auto_open, title: oOptions.title, @@ -326,14 +327,17 @@ CombodoModal._InstantiateModal = function(oModalElem, oOptions) { */ CombodoModal._ConvertButtonDefinition = function(aButtonsDefinitions){ const aConverted = []; + if(aButtonsDefinitions === null) { + return aConverted + } aButtonsDefinitions.forEach(element => { - const aButton = { - text: element.text, - class: element.class, - click: element.callback_on_click + const aButton = { + text: element.text, + class: element.class, + click: element.callback_on_click + } + aConverted.push(aButton); } - aConverted.push(aButton); - } ); return aConverted; } @@ -428,6 +432,42 @@ CombodoModal.OpenConfirmationModal = function(oOptions, aData) { CombodoModal.OpenModal(oOptions); } +/** + * Open a standard informative modal. + * + * @param sMessage string Informative message to be displayed in the modal + * @param sSeverity string Severity of the information. Default values are success, information, warning, error. + * @param oOptions array @see CombodoModal.OpenModal + */ +CombodoModal.OpenInformativeModal = function(sMessage, sSeverity, oOptions) { + let sFirstLetterUppercaseSeverity = sSeverity.charAt(0).toUpperCase() + sSeverity.slice(1); + // Merge external options with confirmation modal default options + oOptions = $.extend({ + title: Dict.S('UI:Modal:Informative' + sFirstLetterUppercaseSeverity + ':Title'), + classes : { + 'ui-dialog-content': 'ibo-is-informative ibo-is-'+sSeverity, + }, + content: sMessage, + extra_options: { + callback_on_modal_close: function () { + $(this).dialog( "destroy" ); + } + }, + buttons: [ + { + text: Dict.S('UI:Button:Ok'), + class: 'ibo-is-regular ibo-is-neutral', + callback_on_click: function () { + $(this).dialog('close'); + } + }, + ], + }, oOptions); + + // Open modal + CombodoModal.OpenModal(oOptions); +} + // Processing on each pages of the backoffice $(document).ready(function(){ // Initialize global keyboard shortcuts diff --git a/js/utils.js b/js/utils.js index c42ad1a14..77e6aa259 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1167,6 +1167,7 @@ let CombodoModal = { { id: null, // ID of the created modal attributes: {}, // HTML attributes + classes: {}, // Classes for the created modal elements base_modal: { usage: 'clone', // Either 'clone' or 'replace' selector: this._GetDefaultBaseModalSelector() // Either a selector of the modal element used to base this one on or the modal element itself @@ -1304,5 +1305,18 @@ let CombodoModal = { OpenConfirmationModal: function(oOptions) { // Meant for overlaoding CombodoJSConsole.Debug('CombodoModal.OpenConfirmationModal not implemented'); + }, + + + /** + * Open a standard informative modal. + * + * @param sMessage string Informative message to be displayed in the modal + * @param sSeverity string Severity of the information. Default values are success, information, warning, error. + * @param oOptions array @see CombodoModal.OpenModal + */ + OpenInformativeModal: function(sMessage,sSeverity, oOptions) { + // Meant for overlaoding + CombodoJSConsole.Debug('CombodoModal.OpenInformativeModal not implemented'); } }; \ No newline at end of file diff --git a/pages/UI.php b/pages/UI.php index e7f5fc663..ae4356c81 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -786,169 +786,8 @@ try /////////////////////////////////////////////////////////////////////////////////////////// case 'apply_modify': // Applying the modifications to an existing object - $oP->DisableBreadCrumb(); - $sClass = utils::ReadPostedParam('class', '', 'class'); - $sClassLabel = MetaModel::GetName($sClass); - $id = utils::ReadPostedParam('id', ''); - $sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id'); - if ( empty($sClass) || empty($id)) // TO DO: check that the class name is valid ! - { - IssueLog::Trace('Object not updated (empty class or id)', $sClass, array( - '$operation' => $operation, - '$id' => $id, - '$sTransactionId' => $sTransactionId, - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - - throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); - } - $bDisplayDetails = true; - $oObj = MetaModel::GetObject($sClass, $id, false); - if ($oObj == null) - { - $bDisplayDetails = false; - $oP->set_title(Dict::S('UI:ErrorPageTitle')); - $oP->P(Dict::S('UI:ObjectDoesNotExist')); - - IssueLog::Trace('Object not updated (id not found)', $sClass, array( - '$operation' => $operation, - '$id' => $id, - '$sTransactionId' => $sTransactionId, - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - } - elseif (!utils::IsTransactionValid($sTransactionId, false)) - { - //TODO: since $bDisplayDetails= true, there will be an redirection, thus, the content generated here is ignored, only the $sMessage and $sSeverity are used afeter the redirection - $sUser = UserRights::GetUser(); - IssueLog::Error("UI.php '$operation' : invalid transaction_id ! data: user='$sUser', class='$sClass'"); - $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding - $oP->p("".Dict::S('UI:Error:ObjectAlreadyUpdated')."\n"); - $sMessage = Dict::Format('UI:Error:ObjectAlreadyUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'error'; - - IssueLog::Trace('Object not updated (invalid transaction_id)', $sClass, array( - '$operation' => $operation, - '$id' => $id, - '$sTransactionId' => $sTransactionId, - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - } - else - { - $aErrors = $oObj->UpdateObjectFromPostedForm(); - $sMessage = ''; - $sSeverity = 'ok'; - - if (!$oObj->IsModified() && empty($aErrors)) - { - $oP->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding - $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'info'; - - IssueLog::Trace('Object not updated (see either $aErrors or IsModified)', $sClass, array( - '$operation' => $operation, - '$id' => $id, - '$sTransactionId' => $sTransactionId, - '$aErrors' => $aErrors, - 'IsModified' => $oObj->IsModified(), - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - } - else - { - IssueLog::Trace('Object updated', $sClass, array( - '$operation' => $operation, - '$id' => $id, - '$sTransactionId' => $sTransactionId, - '$aErrors' => $aErrors, - 'IsModified' => $oObj->IsModified(), - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - - try - { - if (!empty($aErrors)) - { - throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors)); - } - // Transactions are now handled in DBUpdate - $oObj->DBUpdate(); - $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'ok'; - } - catch (CoreCannotSaveObjectException $e) - { - // Found issues, explain and give the user a second chance - // - $bDisplayDetails = false; - $aIssues = $e->getIssues(); - $oP->AddHeaderMessage($e->getHtmlMessage(), 'message_error'); - $oObj->DisplayModifyForm($oP, - array('wizard_container' => true)); // wizard_container: display the wizard border and the title - } - catch (DeleteException $e) - { - // Say two things: - // - 1) Don't be afraid nothing was modified - $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); - $sSeverity = 'info'; - cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), 'UI:Class_Object_NotUpdated', $sMessage, - $sSeverity, 0, true /* must not exist */); - // - 2) Ok, there was some trouble indeed - $sMessage = $e->getMessage(); - $sSeverity = 'error'; - } - utils::RemoveTransaction($sTransactionId); - } - } - if ($bDisplayDetails) - { - $oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey()); //Workaround: reload the object so that the linkedset are displayed properly - $sNextAction = utils::ReadPostedParam('next_action', ''); - if (!empty($sNextAction)) - { - try - { - ApplyNextAction($oP, $oObj, $sNextAction); - } - catch (ApplicationException $e) - { - $sMessage = $e->getMessage(); - $sSeverity = 'info'; - ReloadAndDisplay($oP, $oObj, 'update', $sMessage, $sSeverity); - } - } - else - { - // Nothing more to do - $sMessage = isset($sMessage) ? $sMessage : ''; - $sSeverity = isset($sSeverity) ? $sSeverity : null; - ReloadAndDisplay($oP, $oObj, 'update', $sMessage, $sSeverity); - } - - $bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled'); - if ($bLockEnabled) - { - // Release the concurrent lock, if any - $sOwnershipToken = utils::ReadPostedParam('ownership_token', null, 'raw_data'); - if ($sOwnershipToken !== null) - { - // We're done, let's release the lock - iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken); - } - } - } + $oController = new ObjectController(); + $oP = $oController->OperationApplyModify(); break; /////////////////////////////////////////////////////////////////////////////////////////// @@ -1047,137 +886,10 @@ try /////////////////////////////////////////////////////////////////////////////////////////// case 'apply_new': // Creation of a new object - $oP->DisableBreadCrumb(); - $sClass = utils::ReadPostedParam('class', '', 'class'); - $sClassLabel = MetaModel::GetName($sClass); - $sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id'); - $aErrors = array(); - $aWarnings = array(); - if ( empty($sClass) ) // TO DO: check that the class name is valid ! - { - IssueLog::Trace('Object not created (empty class)', $sClass, array( - '$operation' => $operation, - '$sTransactionId' => $sTransactionId, - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); - } - if (!utils::IsTransactionValid($sTransactionId, false)) - { - $sUser = UserRights::GetUser(); - IssueLog::Error("UI.php '$operation' : invalid transaction_id ! data: user='$sUser', class='$sClass'"); - $oP->p("".Dict::S('UI:Error:ObjectAlreadyCreated')."\n"); - - IssueLog::Trace('Object not created (invalid transaction_id)', $sClass, array( - '$operation' => $operation, - '$sTransactionId' => $sTransactionId, - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - } - else - { - /** @var \cmdbAbstractObject $oObj */ - $oObj = MetaModel::NewObject($sClass); - if (MetaModel::HasLifecycle($sClass)) - { - $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); - $sTargetState = utils::ReadPostedParam('obj_state', ''); - if ($sTargetState != '') - { - $sOrigState = utils::ReadPostedParam('obj_state_orig', ''); - if ($sTargetState != $sOrigState) - { - $aWarnings[] = Dict::S('UI:StateChanged'); - } - $oObj->Set($sStateAttCode, $sTargetState); - } - } - $aErrors = $oObj->UpdateObjectFromPostedForm(); - } - if (isset($oObj) && is_object($oObj)) - { - $sClass = get_class($oObj); - $sClassLabel = MetaModel::GetName($sClass); - - try - { - if (!empty($aErrors) || !empty($aWarnings)) - { - IssueLog::Trace('Object not created (see $aErrors)', $sClass, array( - '$operation' => $operation, - '$sTransactionId' => $sTransactionId, - '$aErrors' => $aErrors, - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - - throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors)); - } - - $oObj->DBInsertNoReload();// No need to reload - - IssueLog::Trace('Object created', $sClass, array( - '$operation' => $operation, - '$id' => $oObj->GetKey(), - '$sTransactionId' => $sTransactionId, - '$aErrors' => $aErrors, - '$sUser' => UserRights::GetUser(), - 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], - 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], - )); - - utils::RemoveTransaction($sTransactionId); - $oP->set_title(Dict::S('UI:PageTitle:ObjectCreated')); - QuickCreateHelper::AddClassToHistory($sClass); - - // Compute the name, by reloading the object, even if it disappeared from the silo - $oObj = MetaModel::GetObject($sClass, $oObj->GetKey(), true /* Must be found */, true /* Allow All Data*/); - $sName = $oObj->GetName(); - $sMessage = Dict::Format('UI:Title:Object_Of_Class_Created', $sName, $sClassLabel); - - $sNextAction = utils::ReadPostedParam('next_action', ''); - if (!empty($sNextAction)) { - $oP->add("

$sMessage

"); - try { - ApplyNextAction($oP, $oObj, $sNextAction); - } - catch (ApplicationException $e) { - $sMessage = $e->getMessage(); - $sSeverity = 'info'; - ReloadAndDisplay($oP, $oObj, 'create', $sMessage, $sSeverity); - } - } else { - // Nothing more to do - ReloadAndDisplay($oP, $oObj, 'create', $sMessage, 'ok'); - } - } - catch (CoreCannotSaveObjectException $e) { - // Found issues, explain and give the user a second chance - // - $aIssues = $e->getIssues(); - - $sObjKey = $oObj->GetKey(); - $sClassIcon = MetaModel::GetClassIcon($sClass, false); - $sHeaderTitle = Dict::Format('UI:CreationTitle_Class', $sClassLabel); - - $oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel)); - if (!empty($aIssues)) { - $oP->AddHeaderMessage($e->getHtmlMessage(), 'message_error'); - } - if (!empty($aWarnings)) { - $sWarnings = implode(', ', $aWarnings); - $oP->AddHeaderMessage($sWarnings, 'message_warning'); - } - cmdbAbstractObject::DisplayCreationForm($oP, $sClass, $oObj, [], ['transaction_id' => $sTransactionId]); - } - } - break; + $oController = new ObjectController(); + $oP = $oController->OperationApplyNew(); + break; /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/sources/Application/UI/Base/Component/DataTable/DataTable.php b/sources/Application/UI/Base/Component/DataTable/DataTable.php index e57e515fc..e512db814 100644 --- a/sources/Application/UI/Base/Component/DataTable/DataTable.php +++ b/sources/Application/UI/Base/Component/DataTable/DataTable.php @@ -56,6 +56,10 @@ class DataTable extends UIContentBlock * array of data to display the first page */ protected $aInitDisplayData; + /** + * @var string JS Handler to be called when "open_creation_modal.object.itop" is fired on the table + */ + protected string $sModalCreationHandler; public const DEFAULT_ACTION_ROW_CONFIRMATION = true; @@ -73,6 +77,7 @@ class DataTable extends UIContentBlock $this->aOptions = []; $this->aResultColumns = []; $this->sJsonData = ''; + $this->sModalCreationHandler = ''; } /** @@ -260,4 +265,22 @@ class DataTable extends UIContentBlock return []; } + /** + * @return string + */ + public function GetModalCreationHandler(): string + { + return $this->sModalCreationHandler; + } + + /** + * @param string $sModalCreationHandler + * @return $this + */ + public function SetModalCreationHandler(string $sModalCreationHandler) + { + $this->sModalCreationHandler = $sModalCreationHandler; + return $this; + } + } diff --git a/sources/Application/UI/Base/Component/DataTable/DataTableUIBlockFactory.php b/sources/Application/UI/Base/Component/DataTable/DataTableUIBlockFactory.php index 4530eba4a..3bdcf6a3c 100644 --- a/sources/Application/UI/Base/Component/DataTable/DataTableUIBlockFactory.php +++ b/sources/Application/UI/Base/Component/DataTable/DataTableUIBlockFactory.php @@ -772,6 +772,11 @@ class DataTableUIBlockFactory extends AbstractUIBlockFactory $oDataTable->SetRowActions($aExtraParams['row_actions']); } + if (isset($aExtraParams['creation_in_modal_js_handler'])){ + $oDataTable->SetModalCreationHandler($aExtraParams['creation_in_modal_js_handler']); + } + + return $oDataTable; } @@ -1087,6 +1092,10 @@ JS; /**give definition of id for select checkbox*/ 'row_actions', /** array of blocks displayed on every row */ + 'creation_in_modal_is_allowed', + /** bool to allow a creation of a new object of this type in a modal */ + 'creation_in_modal_js_handler', + /** Handler to call when trying to create a new object in modal */ ]; } } \ No newline at end of file diff --git a/sources/Application/UI/Links/AbstractBlockLinksViewTable.php b/sources/Application/UI/Links/AbstractBlockLinksViewTable.php index b5ea3d13e..d15ad92f8 100644 --- a/sources/Application/UI/Links/AbstractBlockLinksViewTable.php +++ b/sources/Application/UI/Links/AbstractBlockLinksViewTable.php @@ -24,6 +24,7 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock public const DEFAULT_JS_TEMPLATE_REL_PATH = 'application/links/layout'; public const DEFAULT_JS_FILES_REL_PATH = [ 'js/links/link_set_worker.js', + 'js/wizardhelper.js', ]; /** @var \DBObject $oDbObject db object witch link set belongs to */ @@ -40,6 +41,8 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock /** @var string $sTargetClass links target classname */ protected string $sTargetClass; + + protected string $sTableId; /** * Constructor. @@ -62,7 +65,8 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock $this->sAttCode = $sAttCode; $this->sObjectClass = $sObjectClass; $this->oDbObject = $oDbObject; - + $this->sTableId = 'rel_'.$this->sAttCode; + $this->SetDataAttributes(['role' => 'ibo-block-links-table', 'link-attcode' => $sAttCode, 'link-class' => $this->oAttDef->GetLinkedClass()]); // Initialization $this->Init(); @@ -131,7 +135,7 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock // add list block $oBlock = new \DisplayBlock($oLinkSet->GetFilter(), 'list', false); - $this->AddSubBlock($oBlock->GetRenderContent($oPage, $this->GetExtraParam(), 'rel_'.$this->sAttCode)); + $this->AddSubBlock($oBlock->GetRenderContent($oPage, $this->GetExtraParam(), $this->sTableId)); } /** @@ -187,4 +191,15 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock * @throws \Exception */ abstract function GetTargetClass(): string; + + + + /** + * @return string + */ + public function GetAttCode(): string + { + return $this->sAttCode; + } + } \ No newline at end of file diff --git a/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php b/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php index dd1a264b1..d32abbfcc 100644 --- a/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php +++ b/sources/Application/UI/Links/Indirect/BlockIndirectLinksViewTable.php @@ -37,7 +37,7 @@ class BlockIndirectLinksViewTable extends AbstractBlockLinksViewTable /** @inheritdoc */ public function GetExtraParam(): array { - return array( + $aExtraParams = array( 'link_attr' => $this->oAttDef->GetExtKeyToMe(), 'object_id' => $this->oDbObject->GetKey(), 'target_attr' => $this->oAttDef->GetExtKeyToRemote(), @@ -48,7 +48,17 @@ class BlockIndirectLinksViewTable extends AbstractBlockLinksViewTable 'zlist' => false, 'extra_fields' => $this->GetAttCodesToDisplay(), 'row_actions' => $this->GetRowActions(), + 'currentId' => $this->GetTableId(), ); + + // - Add creation in modal if the linkset is not readonly + + if (!$this->oAttDef->GetReadOnly()) { + $aExtraParams['creation_in_modal_is_allowed'] = true; + $aExtraParams['creation_in_modal_js_handler'] = 'LinkSetWorker.CreateLinkedObject("'.$this->GetTableId().'");'; + } + + return $aExtraParams; } /** @inheritdoc */ @@ -70,6 +80,12 @@ class BlockIndirectLinksViewTable extends AbstractBlockLinksViewTable ], ); + $aRowActions[] = array( + 'tooltip' => 'UI:Links:ActionRow:Modify', + 'icon_classes' => 'fas fa-pen', + 'js_row_action' => "LinkSetWorker.ModifyLinkedObject('{$this->oAttDef->GetLinkedClass()}', aRowData['Link/_key_/raw'], '{$this->GetTableId()}');", + ); + } return $aRowActions; diff --git a/sources/Application/WebPage/iTopWebPage.php b/sources/Application/WebPage/iTopWebPage.php index ef3494feb..82cb5ffaa 100644 --- a/sources/Application/WebPage/iTopWebPage.php +++ b/sources/Application/WebPage/iTopWebPage.php @@ -215,6 +215,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage // Modals $this->add_dict_entries('UI:Modal:'); + $this->add_dict_entries('UI:Links:'); } /** diff --git a/sources/Controller/Base/Layout/ObjectController.php b/sources/Controller/Base/Layout/ObjectController.php index a341abc21..989ae4f1e 100644 --- a/sources/Controller/Base/Layout/ObjectController.php +++ b/sources/Controller/Base/Layout/ObjectController.php @@ -10,15 +10,19 @@ use AjaxPage; use ApplicationException; use cmdbAbstractObject; use CMDBObjectSet; +use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory; +use Combodo\iTop\Application\UI\Base\Component\QuickCreate\QuickCreateHelper; use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory; use Combodo\iTop\Controller\AbstractController; +use CoreCannotSaveObjectException; use Dict; +use IssueLog; use iTopWebPage; +use JsonPage; use MetaModel; use SecurityException; use utils; use UserRights; -use WebPage; /** * Class ObjectController @@ -65,8 +69,41 @@ class ObjectController extends AbstractController } // Prepare web page (should more likely be some kind of response object like for Symfony) + $aFormExtraParams = array('wizard_container' => 1); if ($this->IsHandlingXmlHttpRequest()) { $oPage = new AjaxPage(''); + $aFormExtraParams['js_handlers'] = []; + $aFormExtraParams['noRelations'] = true; + // We display this form in a modal, once we submit (in ajax) we probably want to only close the modal + $aFormExtraParams['js_handlers']['form_on_submit'] = + <<DisableBreadCrumb(); @@ -78,10 +115,398 @@ class ObjectController extends AbstractController } // Note: Code duplicated to the case 'apply_modify' in UI.php when a data integrity issue has been found - $oObj->DisplayModifyForm($oPage, array('wizard_container' => 1)); // wizard_container: Display the title above the form + $oObj->DisplayModifyForm($oPage, $aFormExtraParams); // wizard_container: Display the title above the form return $oPage; } + + /** + * @return \iTopWebPage|\JsonPage Object edit form in its webpage + * @throws \ApplicationException + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \SecurityException + */ + public function OperationApplyNew() + { + $bPrintable = utils::ReadParam('printable', '0') === '1'; + $aResult = []; + if ($this->IsHandlingXmlHttpRequest()) { + $oPage = new JsonPage(); + $oPage->SetOutputDataOnly(true); + $aResult['success'] = false; + } else { + $oPage = new iTopWebPage('', $bPrintable); + $oPage->DisableBreadCrumb(); + } + + $sClass = utils::ReadPostedParam('class', '', 'class'); + $sClassLabel = MetaModel::GetName($sClass); + $sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id'); + $aErrors = array(); + $aWarnings = array(); + if ( empty($sClass) ) + { + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not created (empty class)', $sClass, array( + '$sTransactionId' => $sTransactionId, + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + + throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class')); + } + if (!utils::IsTransactionValid($sTransactionId, false)) + { + $sUser = UserRights::GetUser(); + IssueLog::Error(__CLASS__.'::'.__METHOD__." : invalid transaction_id ! data: user='$sUser', class='$sClass'"); + + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['data'] = ['error_message' => Dict::S('UI:Error:ObjectAlreadyCreated')]; + } else { + $oErrorAlert = AlertUIBlockFactory::MakeForFailure(Dict::S('UI:Error:ObjectAlreadyCreated')); + $oErrorAlert->SetIsClosable(false) + ->SetIsCollapsible(false); + $oPage->AddUiBlock($oErrorAlert); + } + + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not created (invalid transaction_id)', $sClass, array( + '$sTransactionId' => $sTransactionId, + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + } + else + { + $oObj = MetaModel::NewObject($sClass); + if (MetaModel::HasLifecycle($sClass)) + { + $sStateAttCode = MetaModel::GetStateAttributeCode($sClass); + $sTargetState = utils::ReadPostedParam('obj_state', ''); + if ($sTargetState != '') + { + $sOrigState = utils::ReadPostedParam('obj_state_orig', ''); + if ($sTargetState != $sOrigState) + { + $aWarnings[] = Dict::S('UI:StateChanged'); + } + $oObj->Set($sStateAttCode, $sTargetState); + } + } + $aErrors = $oObj->UpdateObjectFromPostedForm(); + } + if (isset($oObj) && is_object($oObj)) + { + $sClass = get_class($oObj); + $sClassLabel = MetaModel::GetName($sClass); + + try + { + if (!empty($aErrors) || !empty($aWarnings)) + { + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not created (see $aErrors)', $sClass, array( + '$sTransactionId' => $sTransactionId, + '$aErrors' => $aErrors, + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + + throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors)); + } + + $oObj->DBInsertNoReload();// No need to reload + + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object created', $sClass, array( + '$id' => $oObj->GetKey(), + '$sTransactionId' => $sTransactionId, + '$aErrors' => $aErrors, + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + + utils::RemoveTransaction($sTransactionId); + $oPage->set_title(Dict::S('UI:PageTitle:ObjectCreated')); + QuickCreateHelper::AddClassToHistory($sClass); + + // Compute the name, by reloading the object, even if it disappeared from the silo + $oObj = MetaModel::GetObject($sClass, $oObj->GetKey(), true /* Must be found */, true /* Allow All Data*/); + $sName = $oObj->GetName(); + $sMessage = Dict::Format('UI:Title:Object_Of_Class_Created', $sName, $sClassLabel); + + $sNextAction = utils::ReadPostedParam('next_action', ''); + if (!empty($sNextAction)) { + $oPage->add("

$sMessage

"); + try { + ApplyNextAction($oPage, $oObj, $sNextAction); + } + catch (ApplicationException $e) { + $sMessage = $e->getMessage(); + $sSeverity = 'info'; + ReloadAndDisplay($oPage, $oObj, 'create', $sMessage, $sSeverity); + } + } else { + // Nothing more to do + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['success'] = true; + } else { + ReloadAndDisplay($oPage, $oObj, 'create', $sMessage, 'ok'); + } + } + } + catch (CoreCannotSaveObjectException $e) { + // Found issues, explain and give the user a second chance + // + $aIssues = $e->getIssues(); + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['data'] = ['error_message' => $e->getHtmlMessage()]; + } else { + $sObjKey = $oObj->GetKey(); + $sClassIcon = MetaModel::GetClassIcon($sClass, false); + $sHeaderTitle = Dict::Format('UI:CreationTitle_Class', $sClassLabel); + + $oPage->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel)); + if (!empty($aIssues)) { + $oPage->AddHeaderMessage($e->getHtmlMessage(), 'message_error'); + } + if (!empty($aWarnings)) { + $sWarnings = implode(', ', $aWarnings); + $oPage->AddHeaderMessage($sWarnings, 'message_warning'); + } + cmdbAbstractObject::DisplayCreationForm($oPage, $sClass, $oObj, [], ['transaction_id' => $sTransactionId]); + } + } + } + if ($this->IsHandlingXmlHttpRequest()) { + $oPage->SetData($aResult); + } + + return $oPage; + } + + /** + * @return \iTopWebPage|\JsonPage + * @throws \ApplicationException + * @throws \ArchivedObjectException + * @throws \ConfigException + * @throws \CoreException + * @throws \CoreUnexpectedValue + * @throws \DictExceptionMissingString + * @throws \MySQLException + */ + public function OperationApplyModify(){ + $bPrintable = utils::ReadParam('printable', '0') === '1'; + $aResult = []; + if ($this->IsHandlingXmlHttpRequest()) { + $oPage = new JsonPage(); + $oPage->SetOutputDataOnly(true); + $aResult['success'] = false; + } else { + $oPage = new iTopWebPage('', $bPrintable); + $oPage->DisableBreadCrumb(); + } + + $sClass = utils::ReadPostedParam('class', '', 'class'); + $sClassLabel = MetaModel::GetName($sClass); + $id = utils::ReadPostedParam('id', ''); + $sTransactionId = utils::ReadPostedParam('transaction_id', '', 'transaction_id'); + if ( empty($sClass) || empty($id)) + { + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (empty class or id)', $sClass, array( + '$id' => $id, + '$sTransactionId' => $sTransactionId, + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + // TODO 3.1 Do not crash with an exception in ajax + throw new ApplicationException(Dict::Format('UI:Error:2ParametersMissing', 'class', 'id')); + } + $bDisplayDetails = true; + $oObj = MetaModel::GetObject($sClass, $id, false); + if ($oObj === null) + { + $bDisplayDetails = false; + + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['data'] = ['error_message' => Dict::S('UI:ObjectDoesNotExist')]; + } else { + $oPage->set_title(Dict::S('UI:ErrorPageTitle')); + + $oErrorAlert = AlertUIBlockFactory::MakeForFailure(Dict::S('UI:ObjectDoesNotExist')); + $oErrorAlert->SetIsClosable(false) + ->SetIsCollapsible(false); + $oPage->AddUiBlock($oErrorAlert); + + } + + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (id not found)', $sClass, array( + '$id' => $id, + '$sTransactionId' => $sTransactionId, + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + } + elseif (!utils::IsTransactionValid($sTransactionId, false)) + { + //TODO: since $bDisplayDetails= true, there will be an redirection, thus, the content generated here is ignored, only the $sMessage and $sSeverity are used after the redirection + $sUser = UserRights::GetUser(); + IssueLog::Error(__CLASS__.'::'.__METHOD__." : invalid transaction_id ! data: user='$sUser', class='$sClass'"); + + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['data'] = ['error_message' => Dict::S('UI:Error:ObjectAlreadyUpdated')]; + } else { + $oPage->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding + $oPage->p("".Dict::S('UI:Error:ObjectAlreadyUpdated')."\n"); + } + + $sMessage = Dict::Format('UI:Error:ObjectAlreadyUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'error'; + + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (invalid transaction_id)', $sClass, array( + '$id' => $id, + '$sTransactionId' => $sTransactionId, + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + } + else + { + $aErrors = $oObj->UpdateObjectFromPostedForm(); + $sMessage = ''; + $sSeverity = 'ok'; + + if (!$oObj->IsModified() && empty($aErrors)) + { + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['data'] = ['error_message' => Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName())]; + } else { + $oPage->set_title(Dict::Format('UI:ModificationPageTitle_Object_Class', $oObj->GetRawName(), $sClassLabel)); // Set title will take care of the encoding + } + + $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'info'; + + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object not updated (see either $aErrors or IsModified)', $sClass, array( + '$id' => $id, + '$sTransactionId' => $sTransactionId, + '$aErrors' => $aErrors, + 'IsModified' => $oObj->IsModified(), + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + } + else + { + IssueLog::Trace(__CLASS__.'::'.__METHOD__.' Object updated', $sClass, array( + '$id' => $id, + '$sTransactionId' => $sTransactionId, + '$aErrors' => $aErrors, + 'IsModified' => $oObj->IsModified(), + '$sUser' => UserRights::GetUser(), + 'HTTP_REFERER' => @$_SERVER['HTTP_REFERER'], + 'REQUEST_URI' => @$_SERVER['REQUEST_URI'], + )); + + try + { + if (!empty($aErrors)) + { + throw new CoreCannotSaveObjectException(array('id' => $oObj->GetKey(), 'class' => $sClass, 'issues' => $aErrors)); + } + // Transactions are now handled in DBUpdate + $oObj->DBUpdate(); + $sMessage = Dict::Format('UI:Class_Object_Updated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'ok'; + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['success'] = true; + } + } + catch (CoreCannotSaveObjectException $e) + { + // Found issues, explain and give the user a second chance + // + $bDisplayDetails = false; + $aIssues = $e->getIssues(); + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['data'] = ['error_message' => $e->getHtmlMessage()]; + } else { + $oPage->AddHeaderMessage($e->getHtmlMessage(), 'message_error'); + $oObj->DisplayModifyForm($oPage, + array('wizard_container' => true)); // wizard_container: display the wizard border and the title + } + + } + catch (DeleteException $e) + { + if ($this->IsHandlingXmlHttpRequest()) { + $aResult['data'] = ['error_message' => Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName())]; + } else { + // Say two things: + // - 1) Don't be afraid nothing was modified + $sMessage = Dict::Format('UI:Class_Object_NotUpdated', MetaModel::GetName(get_class($oObj)), $oObj->GetName()); + $sSeverity = 'info'; + cmdbAbstractObject::SetSessionMessage(get_class($oObj), $oObj->GetKey(), 'UI:Class_Object_NotUpdated', $sMessage, + $sSeverity, 0, true /* must not exist */); + // - 2) Ok, there was some trouble indeed + $sMessage = $e->getMessage(); + $sSeverity = 'error'; + utils::RemoveTransaction($sTransactionId); + } + } + } + } + if ($bDisplayDetails) + { + $oObj = MetaModel::GetObject(get_class($oObj), $oObj->GetKey()); //Workaround: reload the object so that the linkedset are displayed properly + $sNextAction = utils::ReadPostedParam('next_action', ''); + if (!empty($sNextAction)) + { + try + { + ApplyNextAction($oPage, $oObj, $sNextAction); + } + catch (ApplicationException $e) + { + $sMessage = $e->getMessage(); + $sSeverity = 'info'; + ReloadAndDisplay($oPage, $oObj, 'update', $sMessage, $sSeverity); + } + } + else + { + // Nothing more to do + $sMessage = isset($sMessage) ? $sMessage : ''; + $sSeverity = isset($sSeverity) ? $sSeverity : null; + if ($this->IsHandlingXmlHttpRequest()) { + ; + } else{ + ReloadAndDisplay($oPage, $oObj, 'update', $sMessage, $sSeverity); + } + } + + $bLockEnabled = MetaModel::GetConfig()->Get('concurrent_lock_enabled'); + if ($bLockEnabled) + { + // Release the concurrent lock, if any + $sOwnershipToken = utils::ReadPostedParam('ownership_token', null, 'raw_data'); + if ($sOwnershipToken !== null) + { + // We're done, let's release the lock + iTopOwnershipLock::ReleaseLock(get_class($oObj), $oObj->GetKey(), $sOwnershipToken); + } + } + } + if ($this->IsHandlingXmlHttpRequest()) { + $oPage->SetData($aResult); + } + return $oPage; + } /** * @return string[] Rel. paths (to iTop root folder) of required JS files for object modification (create, edit, stimulus, ...) diff --git a/sources/Controller/Links/LinksetController.php b/sources/Controller/Links/LinksetController.php index 90811a1fa..9c7ba15e8 100644 --- a/sources/Controller/Links/LinksetController.php +++ b/sources/Controller/Links/LinksetController.php @@ -6,8 +6,14 @@ namespace Combodo\iTop\Controller\Links; +use AjaxPage; +use cmdbAbstractObject; +use Combodo\iTop\Application\UI\Base\Layout\PageContent\PageContentFactory; use Combodo\iTop\Controller\AbstractController; +use DBObject; +use iTopWebPage; use MetaModel; +use UserRights; use utils; /** @@ -99,5 +105,97 @@ class LinkSetController extends AbstractController return $oPage; } + /** + * @return \iTopWebPage|\AjaxPage Create edit form in its webpage + * @throws \ApplicationException + * @throws \ArchivedObjectException + * @throws \CoreException + * @throws \SecurityException + */ + public function OperationCreateLinkedObject() + { + $bPrintable = utils::ReadParam('printable', '0') === '1'; + $sProposedRealClass = utils::ReadParam('class', '', false, 'class'); + $sAttCode = utils::ReadParam('att_code', '', false, 'raw'); + $sClass = utils::ReadParam('host_class', '', false, 'class'); + $sId = utils::ReadParam('host_id', '', false, 'integer'); + // For security reasons: check that the "proposed" class is actually a subclass of the linked class + // and that the current user is allowed to create objects of this class + $sRealClass = ''; + + $aSubClasses = MetaModel::EnumChildClasses($sProposedRealClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself + $aPossibleClasses = array(); + foreach ($aSubClasses as $sCandidateClass) { + if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES)) { + if ($sCandidateClass == $sProposedRealClass) { + $sRealClass = $sProposedRealClass; + } + $aPossibleClasses[$sCandidateClass] = MetaModel::GetName($sCandidateClass); + } + } + // Only one of the subclasses can be instantiated... + if (count($aPossibleClasses) == 1) { + $aKeys = array_keys($aPossibleClasses); + $sRealClass = $aKeys[0]; + } + if ($sRealClass != '') { + $oLinksetDef = MetaModel::GetAttributeDef($sClass, $sAttCode); + $sExtKeyToMe = $oLinksetDef->GetExtKeyToMe(); + $aFieldFlags = array(); // TODO 3.1 array($sExtKeyToMe => OPT_ATT_READONLY); + $oObj = DBObject::MakeDefaultInstance($sRealClass); + + if ($this->IsHandlingXmlHttpRequest()) { + $oPage = new AjaxPage(''); + } else { + $oPage = new iTopWebPage('', $bPrintable); + $oPage->DisableBreadCrumb(); + $oPage->SetContentLayout(PageContentFactory::MakeForObjectDetails($oObj, cmdbAbstractObject::ENUM_DISPLAY_MODE_CREATE)); + } + + $oSourceObj = MetaModel::GetObject($sClass, $sId); + + $oObj->Set($sExtKeyToMe, $sId); + $aPrefillParam = array('source_obj' => $oSourceObj); + $oObj->PrefillForm('creation_from_editinplace', $aPrefillParam); + // We display this form in a modal, once we submit (in ajax) we probably want to only close the modal + $sFormOnSubmitJsCode = + << true, + 'fieldsFlags' => $aFieldFlags, + 'js_handlers' => [ + 'form_on_submit' => $sFormOnSubmitJsCode, + 'cancel_button_on_click' => + <<