mirror of
https://github.com/Combodo/iTop.git
synced 2026-04-27 12:38:44 +02:00
N°4408 - Improve transition buttons UX in object details
Transition buttons are now grouped in the "Apply" button when there is not enough space to display them all
This commit is contained in:
@@ -10,6 +10,7 @@ use Combodo\iTop\Application\Search\SearchForm;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Alert\AlertUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Button\Button;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Button\ButtonUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\ButtonGroup\ButtonGroupUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\CollapsibleSection\CollapsibleSection;
|
||||
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableSettings;
|
||||
use Combodo\iTop\Application\UI\Base\Component\DataTable\DataTableUIBlockFactory;
|
||||
@@ -27,6 +28,8 @@ use Combodo\iTop\Application\UI\Base\Component\Input\Select\SelectOptionUIBlockF
|
||||
use Combodo\iTop\Application\UI\Base\Component\Input\SelectUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\MedallionIcon\MedallionIcon;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Panel\PanelUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenu;
|
||||
use Combodo\iTop\Application\UI\Base\Component\PopoverMenu\PopoverMenuItem\JsPopoverMenuItem;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Title\TitleUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Component\Toolbar\ToolbarUIBlockFactory;
|
||||
use Combodo\iTop\Application\UI\Base\Layout\ActivityPanel\ActivityPanel;
|
||||
@@ -2805,24 +2808,41 @@ JS
|
||||
|
||||
$aTransitions = $this->EnumTransitions();
|
||||
if (!isset($aExtraParams['custom_operation']) && count($aTransitions)) {
|
||||
// transitions are displayed only for the standard new/modify actions, not for modify_all or any other case...
|
||||
// Transitions are displayed only for the standard new/modify actions, not for modify_all or any other case...
|
||||
$oSetToCheckRights = DBObjectSet::FromObject($this);
|
||||
|
||||
$oTransitionPopoverMenu = new PopoverMenu();
|
||||
$sTPMSectionId = 'transitions';
|
||||
$oTransitionPopoverMenu->AddSection($sTPMSectionId);
|
||||
$aStimuli = Metamodel::EnumStimuli($sClass);
|
||||
foreach ($aTransitions as $sStimulusCode => $aTransitionDef) {
|
||||
$iActionAllowed = (get_class($aStimuli[$sStimulusCode]) == 'StimulusUserAction') ? UserRights::IsStimulusAllowed($sClass,
|
||||
$sStimulusCode, $oSetToCheckRights) : UR_ALLOWED_NO;
|
||||
switch ($iActionAllowed) {
|
||||
case UR_ALLOWED_YES:
|
||||
// Button to be displayed on its own on large screens
|
||||
$oButton = ButtonUIBlockFactory::MakeForPrimaryAction($aStimuli[$sStimulusCode]->GetLabel(), 'next_action', $sStimulusCode, true);
|
||||
$oButton->AddCSSClass('action');
|
||||
$oButton->SetColor(Button::ENUM_COLOR_SCHEME_NEUTRAL);
|
||||
$oToolbarButtons->AddSubBlock($oButton);
|
||||
|
||||
// Button to be displayed in a grouped button on smaller screens
|
||||
$oTPMPopupMenuItem = new JSPopupMenuItem('next_action--'.$oButton->GetId(), $oButton->GetLabel(), "$(`#{$oButton->GetId()}`).trigger(`click`);");
|
||||
$oTransitionPopoverMenu->AddItem($sTPMSectionId, new JsPopoverMenuItem($oTPMPopupMenuItem));
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
// If there are some allowed transitions, build the grouped button
|
||||
if ($oTransitionPopoverMenu->HasItems()) {
|
||||
$oApplyForButtonGroup = ButtonUIBlockFactory::MakeForPrimaryAction($oApplyButton->GetLabel(), null, null, true);
|
||||
$oApplyAndTransitionsButtonGroup = ButtonGroupUIBlockFactory::MakeButtonWithOptionsMenu($oApplyForButtonGroup, $oTransitionPopoverMenu)
|
||||
->SetIsHidden(true);
|
||||
$oToolbarButtons->AddSubBlock($oApplyAndTransitionsButtonGroup);
|
||||
}
|
||||
}
|
||||
|
||||
$sStatesSelection = '';
|
||||
|
||||
@@ -51,6 +51,13 @@ $ibo-object-details--tag--separator--border-radius: $ibo-border-radius-full !def
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Direct child selector is mandatory, otherwise a panel within a panel will have be impacted (eg. n:n relations tabs)
|
||||
> .ibo-panel--header {
|
||||
.ibo-panel--subtitle {
|
||||
@extend %ibo-text-truncated-with-ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ibo-object-details--status {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -25,6 +25,8 @@ $(function()
|
||||
_create: function()
|
||||
{
|
||||
this._super();
|
||||
|
||||
this._initializeSubmittingButtonsObserver();
|
||||
},
|
||||
// events bound via _bind are removed automatically
|
||||
// revert other modifications here
|
||||
@@ -38,7 +40,8 @@ $(function()
|
||||
this._super();
|
||||
|
||||
// Keep URL's hash parameters when clicking on a link of the header
|
||||
this.element.on('click', '[data-role="ibo-panel--header-right"] a', function() {
|
||||
// Note: ":first" used to only target the header of the object, not what could be in the content of its body
|
||||
this.element.on('click', '[data-role="ibo-panel--header-right"]:first a', function() {
|
||||
aMatches = /#(.*)$/.exec(window.location.href);
|
||||
if (aMatches != null) {
|
||||
currentHash = aMatches[1];
|
||||
@@ -48,5 +51,73 @@ $(function()
|
||||
}
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Initialize the observer on the submitting buttons (cancel, apply, transitions, ...) to display only the grouped button depending on the available space
|
||||
* @private
|
||||
*/
|
||||
_initializeSubmittingButtonsObserver: function()
|
||||
{
|
||||
// This only applies in edit mode
|
||||
if (this._getObjectMode() !== 'edit') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If no ResizeObserver, fallback is that transition buttons will overflow on smaller screen
|
||||
if (window.ResizeObserver === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if transition available
|
||||
const oHeaderElem = this.element.find('[data-role="ibo-panel--header"]:first');
|
||||
const oButtonsToolbarElem = oHeaderElem.find('[data-role="ibo-panel--header-right"] [data-role="ibo-toolbar"]');
|
||||
const oTransitionButtonsElems = oButtonsToolbarElem.find('[name="next_action"][data-role="ibo-button"]');
|
||||
if (oHeaderElem.find('[name="next_action"][data-role="ibo-button"]').length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let iCurrentHeaderWidth = 0;
|
||||
let hTimeout = null;
|
||||
const oObserver = new ResizeObserver(function(aEntries) {
|
||||
// We hide the transition buttons during the resize to minimize visual glitches
|
||||
oTransitionButtonsElems.addClass('ibo-is-hidden');
|
||||
// Throttle the processing in order to limit CPU usage
|
||||
clearTimeout(hTimeout);
|
||||
hTimeout = setTimeout(() => {
|
||||
let iNewHeaderWidth = parseInt(oHeaderElem.outerWidth());
|
||||
if (Math.abs(iNewHeaderWidth - iCurrentHeaderWidth) < 5) {
|
||||
return;
|
||||
}
|
||||
oTransitionButtonsElems.removeClass('ibo-is-hidden');
|
||||
|
||||
let oLastTransitionButton = oButtonsToolbarElem.find('[name="next_action"][data-role="ibo-button"]:last');
|
||||
let iLastTransitionButtonBorderY = oLastTransitionButton.offset().left + oLastTransitionButton.outerWidth();
|
||||
let iPanelRightBorderY = oHeaderElem.offset().left + oHeaderElem.outerWidth();
|
||||
|
||||
if (iLastTransitionButtonBorderY > iPanelRightBorderY) {
|
||||
oButtonsToolbarElem.find('.action[data-role="ibo-button"]:not([name="cancel"])').addClass('ibo-is-hidden');
|
||||
oButtonsToolbarElem.find('[data-role="ibo-button-group"]:last').removeClass('ibo-is-hidden');
|
||||
}
|
||||
else {
|
||||
oButtonsToolbarElem.find('.action[data-role="ibo-button"]:not([name="cancel"])').removeClass('ibo-is-hidden');
|
||||
oButtonsToolbarElem.find('[data-role="ibo-button-group"]:last').addClass('ibo-is-hidden');
|
||||
}
|
||||
|
||||
iCurrentHeaderWidth = parseInt(oHeaderElem.outerWidth());
|
||||
|
||||
}, 100);
|
||||
});
|
||||
// Note: ":first" used to only target the header of the object, not what could be in the content of its body
|
||||
oObserver.observe(oHeaderElem[0]);
|
||||
},
|
||||
|
||||
// Helpers
|
||||
/**
|
||||
* @return {String} The current object display mode ({@see PHP \cmdbAbstractObject for possible values})
|
||||
* @private
|
||||
*/
|
||||
_getObjectMode: function()
|
||||
{
|
||||
return this.element.attr('data-object-mode');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user