N°5976 - Add modal creation for linksets displayed with tagset-like widget (#469)

This commit is contained in:
Stephen Abello
2023-03-31 10:32:07 +02:00
committed by GitHub
parent a80506b8e8
commit 2758aaaa89
15 changed files with 253 additions and 111 deletions

View File

@@ -3113,10 +3113,22 @@ EOF
$sClassIconUrl = MetaModel::GetClassIcon($sClass, false);
$oPanel = PanelUIBlockFactory::MakeForClass($sClass, $sTitle)
->SetIcon($sClassIconUrl);
$oPanel->AddMainBlock(self::DisplayFormBlockSelectClassToCreate($sClass, $sClassLabel, $oAppContext, $aPossibleClasses, $aHiddenFields));
$oP->AddSubBlock($oPanel);
}
/**
* @param string $sClass
* @param string $sClassLabel
* @param array $aPossibleClasses
*
* @return \Combodo\iTop\Application\UI\Base\Component\Form\Form
* @throws \CoreException
*/
public static function DisplayFormBlockSelectClassToCreate( string $sClass, string $sClassLabel, ApplicationContext $oAppContext, array $aPossibleClasses, array $aHiddenFields): Form
{
$oClassForm = FormUIBlockFactory::MakeStandard();
$oPanel->AddMainBlock($oClassForm);
$oClassForm->AddHtml($oAppContext->GetForForm())
->AddSubBlock(InputUIBlockFactory::MakeForHidden('checkSubclass', '0'))
@@ -3149,10 +3161,8 @@ EOF
}
$oClassForm->AddSubBlock(self::DisplayBlockSelectClassToCreate($sClass, $sClassLabel, $aPossibleClasses));
$oP->AddSubBlock($oPanel);
return $oClassForm;
}
/**
* @param string $sClassLabel
* @param array $aPossibleClasses

View File

@@ -33,15 +33,19 @@ $ibo-vendors-selectize--input-error--border: 1px solid $ibo-color-red-600 !defau
display: flex;
.selectize-add-option {
position: absolute;
right: $ibo-vendors-selectize-control--plugin-add-button--add-option--right;
display: inline-flex;
justify-content: center;
align-items: center;
position: absolute;
right: $ibo-vendors-selectize-control--plugin-add-button--add-option--right;
height: $ibo-vendors-selectize-control--plugin-add-button--add-option--height;
width: $ibo-vendors-selectize-control--plugin-add-button--add-option--width;
z-index: 1;
color: $ibo-vendors-selectize-control--plugin-add-button--add-option--color;
@extend %ibo-font-size-100;
}
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* Copyright (C) 2013-2023 Combodo SARL
*
* This file is part of iTop.
*
* iTop 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.
*
* iTop 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
*/
Dict::Add('EN US', 'English', 'English', array(
'UI:Object:Modal:Title' => 'Create an object',
));

View File

@@ -1,4 +1,4 @@
let CombodoLinkSetWorker = new function(){
const iTopLinkSetWorker = new function(){
// defines
const ROUTER_BASE_URL = '../pages/ajax.render.php';

View File

@@ -1,4 +1,4 @@
let CombodoLinkSet = new function () {
const iTopLinkSet = new function () {
/**
* Create a new link object and add it to set widget.
@@ -12,18 +12,17 @@ let CombodoLinkSet = new function () {
* @param oWidget
* @constructor
*/
const CallCreateLinkedObject = function(sLinkedClass, sCode, sHostObjectClass, sHostObjectKey, sRemoteExtKey, sRemoteClass, oWidget)
const CallCreateLinkedObject = function(sLinkedClass, oWidget)
{
// Create link object
CombodoLinkSetWorker.CreateLinkedObject(sLinkedClass, sCode, sHostObjectClass, sHostObjectKey,
function(){
iTopObjectWorker.CreateObject(sLinkedClass, function(){
$(this).find("form").remove();
$(this).dialog('destroy');
},
function(event, data){
// We have just create a link object, now request the remote object
CombodoLinkSetWorker.GetRemoteObject(data.data.object.class_name, data.data.object.key, sRemoteExtKey, sRemoteClass, function(data){
iTopObjectWorker.GetObject(data.data.object.class_name, data.data.object.key, function(data){
// Add the new remote object in widget set options list
const selectize = oWidget[0].selectize;
@@ -32,9 +31,6 @@ let CombodoLinkSet = new function () {
// Select the new remote object
selectize.addItem(data.data.object.key);
// Add to initial values, to handle remove action
selectize.addInitialValue(data.data.object.key);
});
});
}

View File

@@ -34,7 +34,7 @@ $(function()
const me = this;
// link object deletion
CombodoLinkSetWorker.DeleteLinkedObject(this.options.link_class, sLinkedObjectKey, function (data) {
iTopLinkSetWorker.DeleteLinkedObject(this.options.link_class, sLinkedObjectKey, function (data) {
if (data.data.success === true) {
me.$tableSettingsDialog.DataTableSettings('DoRefresh');
} else {
@@ -55,7 +55,7 @@ $(function()
const me = this;
// link object unlink
CombodoLinkSetWorker.DetachLinkedObject(this.options.link_class, sLinkedObjectKey, this.options.external_key_to_me, function (data) {
iTopLinkSetWorker.DetachLinkedObject(this.options.link_class, sLinkedObjectKey, this.options.external_key_to_me, function (data) {
if (data.data.success === true) {
me.$tableSettingsDialog.DataTableSettings('DoRefresh');
} else {
@@ -82,7 +82,7 @@ $(function()
const sHostObjectId = $Table.closest('[data-role="ibo-object-details"]').attr('data-object-id');
// link object creation
CombodoLinkSetWorker.CreateLinkedObject(sClass, sAttCode, sHostObjectClass, sHostObjectId, function(){
iTopLinkSetWorker.CreateLinkedObject(sClass, sAttCode, sHostObjectClass, sHostObjectId, function(){
$(this).find("form").remove();
$(this).dialog('destroy');
},function (event, data) {
@@ -102,7 +102,7 @@ $(function()
const me = this;
// link object modification
ObjectWorker.ModifyObject(this.options.link_class, sLinkedObjectKey, function () {
iTopObjectWorker.ModifyObject(this.options.link_class, sLinkedObjectKey, function () {
$(this).find("form").remove();
$(this).dialog('destroy');
}, function(event, data){

View File

@@ -1,10 +1,32 @@
let ObjectWorker = new function(){
const iTopObjectWorker = new function(){
// defines
const ROUTER_BASE_URL = '../pages/ajax.render.php';
const ROUTE_CREATE_OBJECT = 'object.new';
const ROUTE_MODIFY_OBJECT = 'object.modify';
const ROUTE_GET_OBJECT = 'object.get';
const CallAjaxCreateObject = function(sClass, oOnModalCloseCallback = null, oOnFormSubmittedCallback = null){
let oOptions = {
title: Dict.S('UI:Object:Modal:Title'),
content: {
endpoint: `${ROUTER_BASE_URL}?route=${ROUTE_CREATE_OBJECT}`,
data: {
class: sClass,
}
},
extra_options: {
callback_on_modal_close: oOnModalCloseCallback
},
}
const oModal = CombodoModal.OpenModal(oOptions);
if(oOnFormSubmittedCallback !== null){
oModal.on('itop.form.submitted', 'form', oOnFormSubmittedCallback);
}
};
/**
* CallAjaxModifyObject.
*
@@ -40,7 +62,7 @@ let ObjectWorker = new function(){
* CallAjaxGetObject.
*
* @param {string} sObjectClass
* @param {string} sObjectKey
* @param {string} sObjectId
* @param oOnResponseCallback
* @constructor
*/
@@ -54,6 +76,7 @@ let ObjectWorker = new function(){
return {
CreateObject: CallAjaxCreateObject,
ModifyObject: CallAjaxModifyObject,
GetObject: CallAjaxGetObject
}

View File

@@ -28,7 +28,7 @@ Selectize.define("combodo_add_button", function (aOptions) {
label: "+",
html: function () {
return (
'<a class="' + this.className + ' fas fa-plus" title="' + this.title + '"></a>'
'<a class="' + this.className + '"><i class="fas fa-plus" title="' + this.title + '"/></a>'
);
},
},

View File

@@ -704,80 +704,12 @@ try
break;
///////////////////////////////////////////////////////////////////////////////////////////
/** @deprecated 3.1.0 Use the "object.new" route instead */
// Kept for backward compatibility
case 'new': // Form to create a new object
$oP->DisableBreadCrumb();
$sClass = utils::ReadParam('class', '', false, 'class');
$sStateCode = utils::ReadParam('state', '');
$bCheckSubClass = utils::ReadParam('checkSubclass', true);
if ( empty($sClass) )
{
throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class'));
}
/*
$aArgs = utils::ReadParam('default', array(), false, 'raw_data');
$aContext = $oAppContext->GetAsHash();
foreach( $oAppContext->GetNames() as $key)
{
$aArgs[$key] = $oAppContext->GetCurrentValue($key);
}
*/
// If the specified class has subclasses, ask the user an instance of which class to create
$aSubClasses = MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself
$aPossibleClasses = array();
$sRealClass = '';
if ($bCheckSubClass)
{
foreach($aSubClasses as $sCandidateClass)
{
if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES))
{
$aPossibleClasses[$sCandidateClass] = MetaModel::GetName($sCandidateClass);
}
}
// Only one of the subclasses can be instantiated...
if (count($aPossibleClasses) == 1)
{
$aKeys = array_keys($aPossibleClasses);
$sRealClass = $aKeys[0];
}
}
else
{
$sRealClass = $sClass;
}
if (!empty($sRealClass))
{
// Set all the default values in an object and clone this "default" object
$oObjToClone = MetaModel::NewObject($sRealClass);
// 1st - set context values
$oAppContext->InitObjectFromContext($oObjToClone);
// 2nd - set values from the page argument 'default'
$oObjToClone->UpdateObjectFromArg('default');
$aPrefillFormParam = array(
'user' => Session::Get('auth_user'),
'context' => $oAppContext->GetAsHash(),
'default' => utils::ReadParam('default', array(), '', 'raw_data'),
'origin' => 'console',
);
// 3rd - prefill API
$oObjToClone->PrefillForm('creation_from_0', $aPrefillFormParam);
// Display the creation form
$sClassLabel = MetaModel::GetName($sRealClass);
$sClassIcon = MetaModel::GetClassIcon($sRealClass);
$sObjectTmpKey = $oObjToClone->GetKey();
$sHeaderTitle = Dict::Format('UI:CreationTitle_Class', $sClassLabel);
// Note: some code has been duplicated to the case 'apply_new' when a data integrity issue has been found
$oP->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel));
$oP->SetContentLayout(PageContentFactory::MakeForObjectDetails($oObjToClone, cmdbAbstractObject::ENUM_DISPLAY_MODE_CREATE));
cmdbAbstractObject::DisplayCreationForm($oP, $sRealClass, $oObjToClone, array(), array('wizard_container' => 1, 'keep_source_object' => true)); // wizard_container: Display the title above the form
} else {
// Select the derived class to create
cmdbAbstractObject::DisplaySelectClassToCreate($sClass, $oP, $oAppContext, $aPossibleClasses,['state' => $sStateCode]);
}
$oController = new ObjectController();
$oP = $oController->OperationNew();
break;
///////////////////////////////////////////////////////////////////////////////////////////

View File

@@ -37,7 +37,8 @@ class Set extends AbstractInput
public const DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH = 'base/components/input/set/layout';
public const DEFAULT_JS_FILES_REL_PATH = [
'js/links/links_set_worker.js',
'js/links/links-set-worker.js',
'js/object/object-worker.js',
'js/selectize/plugin_combodo_add_button.js',
'js/selectize/plugin_combodo_auto_position.js',
'js/selectize/plugin_combodo_update_operations.js',

View File

@@ -36,8 +36,8 @@ abstract class AbstractBlockLinksViewTable extends UIContentBlock
public const DEFAULT_JS_ON_READY_TEMPLATE_REL_PATH = 'application/links/layout';
public const DEFAULT_JS_FILES_REL_PATH = [
'js/links/links_view_table_widget.js',
'js/links/links_set_worker.js',
'js/objects/objects_worker.js',
'js/links/links-set-worker.js',
'js/object/object-worker.js',
'js/wizardhelper.js',
];

View File

@@ -59,18 +59,14 @@ class LinksSetUIBlockFactory extends SetUIBlockFactory
// Set UI block for OQL
$oSetUIBlock = SetUIBlockFactory::MakeForOQL($sId, $sTargetClass, $oAttDef->GetValuesDef()->GetFilterExpression(), $sWizardHelperJsVarName);
$oSetUIBlock->AddJsFileRelPath('js/links/links_set.js');
$oSetUIBlock->AddJsFileRelPath('js/links/links-set.js');
// Remove add button for 3_1_lot1
// Linkset controller OperationCreateLinkedObject need the host object to exist, so if we are in creation of the host object (id=-1) the linked object creation doesn't work.
//
// // Add button behaviour
// if (in_array($oAttDef->GetEditMode(), [LINKSET_EDITMODE_ADDREMOVE, LINKSET_EDITMODE_ADDONLY, LINKSET_EDITMODE_INPLACE, LINKSET_EDITMODE_ACTIONS])
// && $oHostDbObject !== null) {
// $sHostClass = get_class($oHostDbObject);
// $oSetUIBlock->SetHasAddOptionButton(true);
// $oSetUIBlock->SetAddOptionButtonJsOnClick("CombodoLinkSet.CreateLinkedObject('{$oAttDef->GetLinkedClass()}', '{$oAttDef->GetCode()}', '{$sHostClass}', '{$oHostDbObject->GetKey()}', '{$sTargetField}', '{$sTargetClass}', oWidget{$oSetUIBlock->GetId()} );");
// }
// Add button behaviour
if (in_array($oAttDef->GetEditMode(), [LINKSET_EDITMODE_ADDREMOVE, LINKSET_EDITMODE_ADDONLY, LINKSET_EDITMODE_INPLACE, LINKSET_EDITMODE_ACTIONS])
&& $oHostDbObject !== null) {
$oSetUIBlock->SetHasAddOptionButton(true);
$oSetUIBlock->SetAddOptionButtonJsOnClick("iTopLinkSet.CreateLinkedObject('{$sTargetClass}', oWidget{$oSetUIBlock->GetId()} );");
}
// Current value
$aCurrentValues = LinkSetDataTransformer::Decode($oDbObjectSet, $sTargetClass, $sTargetField);

View File

@@ -219,6 +219,7 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
// Modals
$this->add_dict_entries('UI:Modal:');
$this->add_dict_entries('UI:Links:');
$this->add_dict_entries('UI:Object:');
$this->add_dict_entry('UI:Layout:ObjectDetails:New:Modal:Title');
}

View File

@@ -7,9 +7,11 @@
namespace Combodo\iTop\Controller\Base\Layout;
use AjaxPage;
use ApplicationContext;
use ApplicationException;
use cmdbAbstractObject;
use CMDBObjectSet;
use Combodo\iTop\Application\Helper\Session;
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;
@@ -40,6 +42,161 @@ class ObjectController extends AbstractController
{
public const ROUTE_NAMESPACE = 'object';
/**
* @throws \CoreException
* @throws \MySQLHasGoneAwayException
* @throws \MySQLException
* @throws \DictExceptionMissingString
* @throws \CoreUnexpectedValue
* @throws \ConfigException
* @throws \ApplicationException
* @throws \MissingQueryArgument
*/
public function OperationNew()
{
$bPrintable = utils::ReadParam('printable', '0') === '1';
$sClass = utils::ReadParam('class', '', false, 'class');
$sStateCode = utils::ReadParam('state', '');
$bCheckSubClass = utils::ReadParam('checkSubclass', true);
$oAppContext = new ApplicationContext();
if ($this->IsHandlingXmlHttpRequest()) {
$oPage = new AjaxPage('');
} else {
$oPage = new iTopWebPage('', $bPrintable);
$oPage->DisableBreadCrumb();
$this->AddRequiredForModificationJsFilesToPage($oPage);
}
if (empty($sClass))
{
throw new ApplicationException(Dict::Format('UI:Error:1ParametersMissing', 'class'));
}
// If the specified class has subclasses, ask the user an instance of which class to create
$aSubClasses = MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL); // Including the specified class itself
$aPossibleClasses = array();
$sRealClass = '';
if ($bCheckSubClass)
{
foreach($aSubClasses as $sCandidateClass)
{
if (!MetaModel::IsAbstract($sCandidateClass) && (UserRights::IsActionAllowed($sCandidateClass, UR_ACTION_MODIFY) == UR_ALLOWED_YES))
{
$aPossibleClasses[$sCandidateClass] = MetaModel::GetName($sCandidateClass);
}
}
// Only one of the subclasses can be instantiated...
if (count($aPossibleClasses) === 1)
{
$aKeys = array_keys($aPossibleClasses);
$sRealClass = $aKeys[0];
}
}
else
{
$sRealClass = $sClass;
}
if (!empty($sRealClass))
{
// Set all the default values in an object and clone this "default" object
$oObjToClone = MetaModel::NewObject($sRealClass);
// 1st - set context values
$oAppContext->InitObjectFromContext($oObjToClone);
// 2nd - set values from the page argument 'default'
$oObjToClone->UpdateObjectFromArg('default');
$aPrefillFormParam = array(
'user' => Session::Get('auth_user'),
'context' => $oAppContext->GetAsHash(),
'default' => utils::ReadParam('default', array(), '', 'raw_data'),
'origin' => 'console',
);
// 3rd - prefill API
$oObjToClone->PrefillForm('creation_from_0', $aPrefillFormParam);
// Display the creation form
$sClassLabel = MetaModel::GetName($sRealClass);
$sClassIcon = MetaModel::GetClassIcon($sRealClass);
$sObjectTmpKey = $oObjToClone->GetKey();
$sHeaderTitle = Dict::Format('UI:CreationTitle_Class', $sClassLabel);
// Note: some code has been duplicated to the case 'apply_new' when a data integrity issue has been found
$aFormExtraParams = array('wizard_container' => 1, 'keep_source_object' => true);
if ($this->IsHandlingXmlHttpRequest()) {
$aFormExtraParams['js_handlers'] = [];
$aFormExtraParams['noRelations'] = true;
$aFormExtraParams['hide_transitions'] = true;
// Add a random prefix to avoid ID collision for form elements
$aFormExtraParams['formPrefix'] = utils::Sanitize(uniqid('', true), '', utils::ENUM_SANITIZATION_FILTER_ELEMENT_IDENTIFIER).'_';
// 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'] =
<<<JS
event.preventDefault();
if(bOnSubmitForm === true)
{
let oForm = $(this);
let sUrl = oForm.attr('action');
let sPosting = $.post( sUrl, oForm.serialize());
/* Alerts the results */
sPosting.done(function(data) {
// fire event
oForm.trigger('itop.form.submitted', [data]);
if(data.success !== undefined && data.success === true) {
oForm.closest('[data-role="ibo-modal"]').dialog('close');
}
else {
/* We're not in submit anymore */
window.bInSubmit = false;
oForm.attr('data-form-state', 'default');
/* Display error popup */
CombodoModal.OpenInformativeModal(data.data.error_message, 'error');
}
});
}
JS;
$aFormExtraParams['js_handlers']['cancel_button_on_click'] =
<<<JS
function() {
$(this).closest('[data-role="ibo-modal"]').dialog('close');
};
JS;
} else {
$oPage->set_title(Dict::Format('UI:CreationPageTitle_Class', $sClassLabel));
$oPage->SetContentLayout(PageContentFactory::MakeForObjectDetails($oObjToClone, cmdbAbstractObject::ENUM_DISPLAY_MODE_CREATE));
}
cmdbAbstractObject::DisplayCreationForm($oPage, $sRealClass, $oObjToClone, array(), $aFormExtraParams);
} else {
if ($this->IsHandlingXmlHttpRequest()) {
$oClassForm = cmdbAbstractObject::DisplayFormBlockSelectClassToCreate($sClass, MetaModel::GetName($sClass), $oAppContext, $aPossibleClasses, ['state' => $sStateCode]);
$sCurrentUrl = utils::GetAbsoluteUrlAppRoot().'/pages/UI.php?route=object.new';
$oClassForm->SetOnSubmitJsCode(
<<<JS
let me = this;
let aParam = {};
aParam['class'] = $(this).find('[name="class"]').val();
let sPosting = $.post('$sCurrentUrl', aParam);
sPosting.done(function(data){
$(me).closest('[data-role="ibo-modal"]').html(data);
$(me).closest('[data-role="ibo-modal"]').dialog({ position: { my: "center", at: "center", of: window }});;
});
return false;
JS
);
$oPage->AddUiBlock($oClassForm);
}
else{
cmdbAbstractObject::DisplaySelectClassToCreate($sClass, $oPage, $oAppContext, $aPossibleClasses,['state' => $sStateCode]);
}
}
return $oPage;
}
/**
* @return \iTopWebPage|\AjaxPage Object edit form in its webpage
* @throws \ApplicationException
@@ -603,7 +760,7 @@ JS;
// Retrieve query params
$sObjectClass = utils::ReadParam('object_class', '', false, utils::ENUM_SANITIZATION_FILTER_STRING);
$sObjectKey = utils::ReadParam('object_key', '', false, utils::ENUM_SANITIZATION_FILTER_STRING);
$sObjectKey = utils::ReadParam('object_key', 0, false, utils::ENUM_SANITIZATION_FILTER_INTEGER);
// Retrieve object
try {

View File

@@ -270,7 +270,7 @@ JS
try {
$oObject = MetaModel::GetObject($sObjectClass, $sObjectKey);
$sLinkKey = $oObject->GetKey();
if ($sRemoteExtKey !== null) {
if (!utils::IsNullOrEmptyString($sRemoteExtKey)) {
$oObject = MetaModel::GetObject($sRemoteClass, $oObject->Get($sRemoteExtKey));
}
$aObjectData = ObjectRepository::ConvertObjectToArray($oObject, $sObjectClass);