N°2847 - Rework iTopWebPage layout (WIP Part IV)

- iTopWebPage: Clean up some commented sections
- iTopWebPage: Marked some new methods as @internal
- iTopWebPage: Restore page content to allow // developments
- Quick create: Add quick object creation box to the top bar
- Global search: Improve cinematic with other widgets
- Components / Layouts: Move JS parts to iTopWebPage, will be put in dedicated PHP helpers later
This commit is contained in:
Molkobain
2020-07-27 15:07:40 +02:00
parent d56f6e684a
commit d4694b271f
18 changed files with 726 additions and 76 deletions

View File

@@ -22,6 +22,7 @@ require_once(APPROOT."/application/applicationcontext.class.inc.php");
require_once(APPROOT."/application/user.preferences.class.inc.php");
use Combodo\iTop\Application\Branding;
use Combodo\iTop\Application\QuickCreate\QuickCreateHelper;
use Combodo\iTop\Application\GlobalSearch\GlobalSearchHelper;
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
@@ -175,24 +176,6 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
*/
protected function PrepareLayout()
{
// TODO: Move this to the menu renderer
// if (MetaModel::GetConfig()->Get('demo_mode'))
// {
// // No pin button
// $sConfigureWestPane = '';
// }
// else
// {
// $sConfigureWestPane =
// <<<EOF
// if (typeof myLayout !== "undefined")
// {
// myLayout.addPinBtn( "#tPinMenu", "west" );
// }
//EOF;
// }
// $sInitClosed = $this->IsMenuPaneVisible() ? '' : 'initClosed: true,';
$sJSDisconnectedMessage = json_encode(Dict::S('UI:DisconnectedDlgMessage'));
$sJSTitle = json_encode(Dict::S('UI:DisconnectedDlgTitle'));
$sJSLoginAgain = json_encode(Dict::S('UI:LoginAgain'));
@@ -305,6 +288,8 @@ class iTopWebPage extends NiceWebPage implements iTabbedPage
JS
);
// TODO: This is for tag sets, refactor the attribute markup so it contains the necessary
// TODO: data-tooltip-* attributes to activate the tooltips automatically (see /js/pages/backoffice.js)
// Attribute set tooltip on items
$this->add_ready_script(
<<<JS
@@ -336,6 +321,8 @@ JS
});
JS
);
// TODO: Change CSS class and extract this in backoffice.js
// Make image attributes zoomable
$this->add_ready_script(
<<<JS
@@ -345,7 +332,8 @@ JS
.magnificPopup({type: 'image', closeOnContentClick: true });
JS
);
// TODO: Change CSS class and extract this in backoffice.js
// Highlight code content created with CKEditor
$this->add_ready_script(
<<<JS
@@ -852,12 +840,13 @@ JS
* @throws \Exception
* @throws \DictExceptionMissingString
* @since 2.8.0
* @internal
*/
protected function GetNavigationMenuData()
{
$oAppContext = new ApplicationContext();
return [
$aData = [
'sId' => 'ibo-navigation-menu',
'sAppRevisionNumber' => $this->GetApplicationRevisionNumber(),
'sAppSquareIconUrl' => Branding::GetSquareMainLogoAbsoluteUrl(),
@@ -866,6 +855,16 @@ JS
'aMenuGroups' => ApplicationMenu::GetMenuGroups($oAppContext->GetAsHash()),
'bIsExpanded' => $this->IsMenuPaneVisible(),
];
// TODO: Move this in the PHP component when designed
$this->add_linked_script('../js/layouts/navigation-menu.js');
// ... and this in a dedicated JS TWIG
$this->add_ready_script(<<<JS
$('#{$aData['sId']}').navigation_menu();
JS
);
return $aData;
}
/**
@@ -874,13 +873,16 @@ JS
* @return array
* @throws \ConfigException
* @throws \CoreException
* @throws \Exception
* @since 2.8.0
* @internal
*/
protected function GetTopBarData()
{
$aData = [
'sId' => 'ibo-top-bar',
'aComponents' => [
'aQuickCreate' => $this->GetQuickCreateData(),
'aGlobalSearch' => $this->GetGlobalSearchData(),
'aBreadCrumbs' => $this->GetBreadCrumbsData(),
],
@@ -889,12 +891,43 @@ JS
return $aData;
}
/**
* Return the quick create data (last classes)
*
* @return array
* @throws \Exception
* @since 2.8.0
* @internal
*/
protected function GetQuickCreateData()
{
$aData = [
'sId' => 'ibo-quick-create',
'sEndpoint' => utils::GetAbsoluteUrlAppRoot().'pages/UI.php',
'aAvailableClasses' => UserRights::GetAllowedClasses(UR_ACTION_CREATE, array('bizmodel'), true),
'aLastClasses' => QuickCreateHelper::GetLastClasses(),
];
// TODO: Move this in the PHP component when designed
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/selectize.default.css');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/selectize.min.js');
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/components/quick-create.js');
// ... and this in a dedicated JS TWIG
$this->add_ready_script(<<<JS
$('#{$aData['sId']}').quick_create();
JS
);
return $aData;
}
/**
* Return the global search data (last queries)
*
* @return array
* @throws \Exception
* @since 2.8.0
* @internal
*/
protected function GetGlobalSearchData()
{
@@ -904,6 +937,14 @@ JS
'aLastQueries' => GlobalSearchHelper::GetLastQueries(),
];
// TODO: Move this in the PHP component when designed
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/components/global-search.js');
// ... and this in a dedicated JS TWIG
$this->add_ready_script(<<<JS
$('#{$aData['sId']}').global_search();
JS
);
return $aData;
}
@@ -914,11 +955,13 @@ JS
* @throws \ConfigException
* @throws \CoreException
* @since 2.8.0
* @internal
*/
protected function GetBreadCrumbsData()
{
$aData = [
'sId' => 'ibo-breadcrumbs',
'aWidgetOptions' => [],
];
$iBreadCrumbMaxCount = utils::GetConfig()->Get('breadcrumb.max_count');
@@ -961,6 +1004,16 @@ JS
];
}
// TODO: Move this in the PHP component when designed
$this->add_linked_script(utils::GetAbsoluteUrlAppRoot().'js/components/breadcrumbs.js');
// ... and this in a dedicated JS TWIG
$sWidgetOptionsAsJson = json_encode($aData['aWidgetOptions']);
$this->add_ready_script(<<<JS
// Note: When refactor in the JS TWIG, use {{ aBreadCrumbs.aWidgetOptions|json_encode|raw }}
$('#{$aData['sId']}').breadcrumbs($sWidgetOptionsAsJson);
JS
);
return $aData;
}
@@ -1029,6 +1082,7 @@ EOF
* Render the banner HTML which can come from both iTop itself and from extensions
*
* @see \iPageUIExtension::GetBannerHtml()
* @internal
*
* @return string
* @since 2.8.0
@@ -1050,6 +1104,7 @@ EOF
* Render the header HTML which can come from both iTop itself and from extensions
*
* @see \iPageUIExtension::GetNorthPaneHtml()
* @internal
*
* @return string
* @since 2.8.0
@@ -1077,6 +1132,7 @@ EOF
* Render the footer HTML which can come from both iTop itself and from extensions
*
* @see \iPageUIExtension::GetSouthPaneHtml()
* @internal
*
* @return string
* @since 2.8.0
@@ -1110,6 +1166,10 @@ EOF
// - Generate necessary dict. files
$this->output_dict_entries();
// TODO: Check if we can keep this as is
// Render the tabs in the page (if any)
$this->s_content = $this->m_oTabs->RenderIntoContent($this->s_content, $this);
// Base structure of data to pass to the TWIG template
$aData['aPage'] = [
'sAbsoluteUrlAppRoot' => $sAbsoluteUrlAppRoot,
@@ -1119,14 +1179,6 @@ EOF
'sCharset' => static::PAGES_CHARSET,
'sLang' => $sMetadataLanguage,
],
'aCssFiles' => $this->a_linked_stylesheets,
'aCssInline' => $this->a_styles,
'aJsFiles' => $this->a_linked_scripts,
'aJsInlineOnInit' => $this->m_aInitScript,
'aJsInlineOnDomReady' => $this->m_aReadyScripts,
'aJsInlineLive' => $this->a_scripts,
// TODO: TEMP, used while developping, remove it.
'aSanitizedContent' => self::FilterXSS($this->s_content),
];
// Base tag
@@ -1149,6 +1201,21 @@ EOF
// - Top bar
$aData['aLayouts']['aTopBar'] = $this->GetTopBarData();
// Variable content of the page
$aData['aPage'] = array_merge(
$aData['aPage'],
[
'aCssFiles' => $this->a_linked_stylesheets,
'aCssInline' => $this->a_styles,
'aJsFiles' => $this->a_linked_scripts,
'aJsInlineOnInit' => $this->m_aInitScript,
'aJsInlineOnDomReady' => $this->m_aReadyScripts,
'aJsInlineLive' => $this->a_scripts,
// TODO: TEMP, used while developping, remove it.
'aSanitizedContent' => self::FilterXSS($this->s_content),
]
);
$oTwigEnv = TwigHelper::GetTwigEnvironment(APPROOT.'templates/');
$sTemplateRelPath = 'pages/backoffice/layout';

View File

@@ -17,4 +17,5 @@
*/
@import "breadcrumbs";
@import "quick-create";
@import "global-search";

View File

@@ -0,0 +1,222 @@
/*!
* Copyright (C) 2013-2020 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
*/
/* SCSS variables */
$ibo-quick-create--head--background-color: $ibo-color-white-100 !default;
$ibo-quick-create--icon-padding-x: 16px !default;
$ibo-quick-create--icon-padding-y: 0 !default;
$ibo-quick-create--input--padding: 0 default;
$ibo-quick-create--input--padding-x--is-opened: 8px !default;
$ibo-quick-create--input--padding-y--is-opened: 8px !default;
$ibo-quick-create--input--width: 0 !default;
$ibo-quick-create--input--width--is-opened: 245px !default;
$ibo-quick-create--input--text-color: $ibo-color-grey-800 !default;
$ibo-quick-create--input--placeholder-color: $ibo-color-grey-600 !default;
$ibo-quick-create--input-options--background-color: $ibo-quick-create--head--background-color !default;
$ibo-quick-create--input-options--border: none !default;
$ibo-quick-create--input-options--border-radius: 0 !default;
$ibo-quick-create--drawer--max-height: 200px !default;
$ibo-quick-create--drawer--padding-x: $ibo-quick-create--icon-padding-x !default;
$ibo-quick-create--drawer--padding-y: 16px !default;
$ibo-quick-create--drawer--top: -1 * ($ibo-quick-create--drawer--max-height) !default;
$ibo-quick-create--drawer--top--is-opened: 100% !default;
$ibo-quick-create--drawer--background-color: $ibo-color-white-100 !default;
$ibo-quick-create--compartment-title--margin-bottom: 8px !default;
$ibo-quick-create--compartment-title--padding-left: 32px !default;
$ibo-quick-create--compartment-title--text-color: $ibo-color-grey-600 !default;
$ibo-quick-create--compartment-title--line-spacing: 8px !default;
$ibo-quick-create--compartment-content--text-color: $ibo-color-grey-900 !default;
$ibo-quick-create--compartment-element--margin-bottom: 8px !default;
$ibo-quick-create--compartment-element-image--margin-right: 8px !default;
$ibo-quick-create--compartment-element-image--width: 20px !default;
$ibo-quick-create--compartment--placeholder-image--margin-top: 24px !default;
$ibo-quick-create--compartment--placeholder-image--margin-bottom: 16px !default;
$ibo-quick-create--compartment--placeholder-image--width: 66% !default;
$ibo-quick-create--compartment--placeholder-hint--padding-x: 8px !default;
$ibo-quick-create--compartment--placeholder-hint--padding-y: 0 !default;
$ibo-quick-create--compartment--placeholder-hint--text-color: $ibo-color-grey-700 !default;
/* Animations*/
@keyframes ibo-quick-create--drawer--opening{
from {
top: $ibo-quick-create--drawer--top;
box-shadow: none;
}
to {
top: $ibo-quick-create--drawer--top--is-opened;
box-shadow: $ibo-elevation-300;
}
}
/* SCSS rules */
.ibo-quick-create{
position: relative;
@extend %ibo-vertically-centered-content;
&.ibo-quick-create--is-opened{
.ibo-quick-create--input{
width: $ibo-quick-create--input--width--is-opened;
}
.ibo-quick-create--drawer{
animation-name: ibo-quick-create--drawer--opening;
animation-delay: 0.1s;
animation-duration: 0.2s;
animation-direction: normal;
animation-fill-mode: forwards;
}
}
}
.ibo-quick-create--head{
@extend %ibo-vertically-centered-content;
background-color: $ibo-quick-create--head--background-color;
}
.ibo-quick-create--icon{
align-self: center;
padding: $ibo-quick-create--icon-padding-y $ibo-quick-create--icon-padding-x;
@extend %ibo-font-ral-nor-400;
}
.ibo-quick-create--input{
width: $ibo-quick-create--input--width;
transition: all 0.2s ease-in-out;
/* Remove selectize.js theme and apply our own */
&.selectize-control.single{
position: sticky;
display: flex;
.selectize-input{
display: flex;
background-color: transparent;
background-image: none;
border: none;
box-shadow: none;
&.input-active{
@extend .selectize-input;
}
> input{
color: $ibo-quick-create--input--text-color;
@extend %ibo-font-ral-nor-300;
outline: none;
border: none;
&::placeholder{
color: $ibo-quick-create--input--placeholder-color;
}
/* This rule is duplicated otherwise Chrome won't be able to parse it. */
&:-ms-input-placeholder,
&::-ms-input-placeholder{
color: $ibo-quick-create--input--placeholder-color;
}
}
}
.selectize-dropdown{
background-color: $ibo-quick-create--input-options--background-color;
border: $ibo-quick-create--input-options--border;
border-radius: $ibo-quick-create--input-options--border-radius;
@extend %ibo-elevation-300;
}
}
}
/* TODO: Make drawer appear below the top bar so its shadow is cast on the drawer */
.ibo-quick-create--drawer{
z-index: -1;
position: absolute;
left: 0;
right: 0;
top: $ibo-quick-create--drawer--top;
padding: $ibo-quick-create--drawer--padding-y $ibo-quick-create--drawer--padding-x;
background-color: $ibo-quick-create--drawer--background-color;
box-shadow: none;
@extend %ibo-font-ral-nor-100;
}
.ibo-quick-create--compartment-title{
margin-bottom: $ibo-quick-create--compartment-title--margin-bottom;
padding-left: $ibo-quick-create--compartment-title--padding-left;
overflow-x: hidden;
color: $ibo-quick-create--compartment-title--text-color;
> span{
position: relative;
&::before,
&::after{
content: "";
display: inline-block;
position: absolute;
top: 50%;
height: 1px;
width: 600px;
border-top: 1px solid $ibo-quick-create--compartment-title--text-color;
}
&::before{
right: 100%;
margin-right: $ibo-quick-create--compartment-title--line-spacing;
}
&::after{
left: 100%;
margin-left: $ibo-quick-create--compartment-title--line-spacing;
}
}
}
.ibo-quick-create--compartment-content{
color: $ibo-quick-create--compartment-content--text-color;
}
.ibo-quick-create--compartment-element{
display: flex;
align-items: center;
color: inherit;
@extend %ibo-text-truncated-with-ellipsis;
&:not(:last-child){
margin-bottom: $ibo-quick-create--compartment-element--margin-bottom;
}
}
.ibo-quick-create--compartment-element-image{
margin-right: $ibo-quick-create--compartment-element-image--margin-right;
width: $ibo-quick-create--compartment-element-image--width;
}
.ibo-quick-create--compartment--placeholder{
align-items: center;
display: flex;
flex-direction: column;
}
.ibo-quick-create--compartment--placeholder-image{
width: $ibo-quick-create--compartment--placeholder-image--width;
margin-top: $ibo-quick-create--compartment--placeholder-image--margin-top;
margin-bottom: $ibo-quick-create--compartment--placeholder-image--margin-bottom;
}
.ibo-quick-create--compartment--placeholder-hint{
text-align: justify;
padding: $ibo-quick-create--compartment--placeholder-hint--padding-y $ibo-quick-create--compartment--placeholder-hint--padding-x;
color: $ibo-quick-create--compartment--placeholder-hint--text-color;
@extend %ibo-font-ral-ita-100;
}

View File

@@ -17,6 +17,10 @@
* You should have received a copy of the GNU Affero General Public License
*/
// Global create
// Quick create
Dict::Add('EN US', 'English', 'English', array(
'UI:Component:QuickCreate:Tooltip' => 'Quickly create any type of object',
'UI:Component:QuickCreate:Input:Placeholder' => 'Select object type...',
'UI:Component:QuickCreate:Recents:Title' => 'Recents',
'UI:Component:QuickCreate:LastClasses:NoClass:Placeholder' => 'You haven\'t create any object yet',
));

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.2 KiB

View File

@@ -55,6 +55,7 @@ $(function()
_bindEvents: function()
{
const me = this;
const oBodyElem = $('body');
this.element.find(this.js_selectors.icon).on('click', function(oEvent){
me._onIconClick(oEvent);
@@ -65,19 +66,30 @@ $(function()
this.element.find(this.js_selectors.compartment_element).on('click', function(oEvent){
me._onCompartmentElementClick(oEvent, $(this));
});
$('body').on('click', function(oEvent){
// Mostly for outside clicks that should close elements
oBodyElem.on('click', function(oEvent){
me._onBodyClick(oEvent);
});
// Mostly for hotkeys
oBodyElem.on('keyup', function(oEvent){
me._onBodyKeyUp(oEvent);
});
},
_onIconClick: function(oEvent)
{
// Avoid anchor glitch
oEvent.preventDefault();
// Open drawer
this.element.toggleClass(this.css_classes.opened);
// Focus in the input for a better UX
this.element.find(this.js_selectors.input).trigger('focus');
if(this._isDrawerOpened())
{
this._closeDrawer();
}
else
{
this._openDrawer();
// Focus in the input for a better UX
this._setFocusOnInput();
}
},
_onFormSubmit: function(oEvent)
{
@@ -103,8 +115,43 @@ $(function()
{
if($(oEvent.target.closest('.ibo-global-search')).length === 0)
{
this.element.removeClass(this.css_classes.opened);
this._closeDrawer();
}
},
_onBodyKeyUp: function(oEvent)
{
// Note: We thought about extracting the oEvent.key in a variable to lower case it, but this would be done
// on every single key up in the application, which might not be what we want... (time consuming)
if((oEvent.altKey === true) && (oEvent.key === 'h' || oEvent.key === 'H'))
{
if(this._isDrawerOpened())
{
this._setFocusOnInput();
}
// If drawer is closed, we trigger the click on the icon in order for the other widget to behave like they should (eg. close themselves)
else
{
this.element.find(this.js_selectors.icon).trigger('click');
}
}
},
// Methods
_isDrawerOpened: function()
{
return this.element.hasClass(this.css_classes.opened);
},
_openDrawer: function()
{
this.element.addClass(this.css_classes.opened);
},
_closeDrawer: function()
{
this.element.removeClass(this.css_classes.opened);
},
_setFocusOnInput: function()
{
this.element.find(this.js_selectors.input).trigger('focus');
}
});
});

View File

@@ -0,0 +1,166 @@
/*
* Copyright (C) 2013-2020 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
*/
;
$(function()
{
// the widget definition, where 'itop' is the namespace,
// 'breadcrumbs' the widget name
$.widget( 'itop.quick_create',
{
// default options
options:
{
},
css_classes:
{
opened: 'ibo-quick-create--is-opened',
},
js_selectors:
{
icon: '[data-role="ibo-quick-create--icon"]',
form: '[data-role="ibo-quick-create--head"]',
input: '[data-role="ibo-quick-create--input"]',
compartment_element: '[data-role="ibo-quick-create--compartment-element"]',
},
// the constructor
_create: function()
{
this.element.addClass('ibo-quick-create');
this._initializeMarkup();
this._bindEvents();
},
// events bound via _bind are removed automatically
// revert other modifications here
_destroy: function()
{
this.element.removeClass('ibo-quick-create');
},
_initializeMarkup: function()
{
const me = this;
// Instantiate selectize.js on input
this.element.find(this.js_selectors.input).selectize({
openOnFocus: false,
maxItems: 1
});
// Remove some inline styling from the widget
this.element.find('.selectize-input > input').css('width', '');
},
_bindEvents: function()
{
const me = this;
const oBodyElem = $('body');
this.element.find(this.js_selectors.icon).on('click', function(oEvent){
me._onIconClick(oEvent);
});
this.element.find(this.js_selectors.form).on('submit', function(oEvent){
me._onFormSubmit(oEvent);
});
this.element.find(this.js_selectors.input).on('change', function(oEvent){
me._onInputOptionSelected(oEvent, $(this));
});
// Mostly for outside clicks that should close elements
oBodyElem.on('click', function(oEvent){
me._onBodyClick(oEvent);
});
// Mostly for hotkeys
oBodyElem.on('keyup', function(oEvent){
me._onBodyKeyUp(oEvent);
});
},
_onIconClick: function(oEvent)
{
// Avoid anchor glitch
oEvent.preventDefault();
if(this._isDrawerOpened())
{
this._closeDrawer();
}
else
{
this._openDrawer();
// Focus in the input for a better UX
this._setFocusOnInput();
}
},
_onFormSubmit: function(oEvent)
{
const sSearchValue = this.element.find(this.js_selectors.input).val();
// Submit form only if something in the input
if(sSearchValue === '')
{
oEvent.preventDefault();
}
},
_onInputOptionSelected: function(oEvent, oInputElem)
{
// Submit form directly on change
this.element.find(this.js_selectors.form).trigger('submit');
},
_onBodyClick: function(oEvent)
{
if($(oEvent.target.closest('.ibo-quick-create')).length === 0)
{
this._closeDrawer();
}
},
_onBodyKeyUp: function(oEvent)
{
// Note: We thought about extracting the oEvent.key in a variable to lower case it, but this would be done
// on every single key up in the application, which might not be what we want... (time consuming)
if((oEvent.altKey === true) && (oEvent.key === 'n' || oEvent.key === 'N'))
{
if(this._isDrawerOpened())
{
this._setFocusOnInput();
}
// If drawer is closed, we trigger the click on the icon in order for the other widget to behave like they should (eg. close themselves)
else
{
this.element.find(this.js_selectors.icon).trigger('click');
}
}
},
// Methods
_isDrawerOpened: function()
{
return this.element.hasClass(this.css_classes.opened);
},
_openDrawer: function()
{
this.element.addClass(this.css_classes.opened);
},
_closeDrawer: function()
{
this.element.removeClass(this.css_classes.opened);
},
_setFocusOnInput: function()
{
this.element.find('.selectize-input > input').trigger('focus');
}
});
});

View File

@@ -60,8 +60,8 @@ $(function()
},
_bindEvents: function()
{
var me = this;
var oBodyElem = $('body');
const me = this;
const oBodyElem = $('body');
// Click on collapse/expand toggler
this.element.find(this.js_selectors.menu_toggler).on('click', function(oEvent){
@@ -241,7 +241,9 @@ $(function()
// Menus filter methods
_focusFilter: function()
{
this.element.find(this.js_selectors.menu_filter_input).trigger('focus');
this.element.find(this.js_selectors.menu_filter_input)
.trigger('click')
.trigger('focus');
},
/**
* Remove the current filter value and reset the menu nodes display

View File

@@ -139,6 +139,7 @@ return array(
'CheckableExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php',
'Combodo\\iTop\\Application\\Branding' => $baseDir . '/sources/application/Branding.php',
'Combodo\\iTop\\Application\\GlobalSearch\\GlobalSearchHelper' => $baseDir . '/sources/application/GlobalSearch/GlobalSearchHelper.php',
'Combodo\\iTop\\Application\\QuickCreate\\QuickCreateHelper' => $baseDir . '/sources/application/QuickCreate/QuickCreateHelper.php',
'Combodo\\iTop\\Application\\Search\\AjaxSearchException' => $baseDir . '/sources/application/search/ajaxsearchexception.class.inc.php',
'Combodo\\iTop\\Application\\Search\\CriterionConversionAbstract' => $baseDir . '/sources/application/search/criterionconversionabstract.class.inc.php',
'Combodo\\iTop\\Application\\Search\\CriterionConversion\\CriterionToOQL' => $baseDir . '/sources/application/search/criterionconversion/criteriontooql.class.inc.php',

View File

@@ -369,6 +369,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'CheckableExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php',
'Combodo\\iTop\\Application\\Branding' => __DIR__ . '/../..' . '/sources/application/Branding.php',
'Combodo\\iTop\\Application\\GlobalSearch\\GlobalSearchHelper' => __DIR__ . '/../..' . '/sources/application/GlobalSearch/GlobalSearchHelper.php',
'Combodo\\iTop\\Application\\QuickCreate\\QuickCreateHelper' => __DIR__ . '/../..' . '/sources/application/QuickCreate/QuickCreateHelper.php',
'Combodo\\iTop\\Application\\Search\\AjaxSearchException' => __DIR__ . '/../..' . '/sources/application/search/ajaxsearchexception.class.inc.php',
'Combodo\\iTop\\Application\\Search\\CriterionConversionAbstract' => __DIR__ . '/../..' . '/sources/application/search/criterionconversionabstract.class.inc.php',
'Combodo\\iTop\\Application\\Search\\CriterionConversion\\CriterionToOQL' => __DIR__ . '/../..' . '/sources/application/search/criterionconversion/criteriontooql.class.inc.php',

View File

@@ -18,6 +18,7 @@
*/
use Combodo\iTop\Application\GlobalSearch\GlobalSearchHelper;
use Combodo\iTop\Application\QuickCreate\QuickCreateHelper;
/**
* Displays a popup welcome message, once per session at maximum
@@ -1266,6 +1267,7 @@ HTML
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*/);

View File

@@ -0,0 +1,123 @@
<?php
/**
* Copyright (C) 2013-2020 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
*/
namespace Combodo\iTop\Application\QuickCreate;
use appUserPreferences;
use DBObject;
use MetaModel;
use utils;
/**
* Class QuickCreateHelper
*
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
* @package Combodo\iTop\Application\QuickCreate
* @since 2.8.0
*/
class QuickCreateHelper
{
const MAX_HISTORY_SIZE = 10;
const USER_PREF_CODE = 'quick_create_history';
/**
* Add $sQuery to the history. History is limited to the static::MAX_HISTORY_SIZE last classes.
*
* @param string $Class Class of the created object
*
* @return void
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
* @throws \Exception
*/
public static function AddClassToHistory($Class)
{
$aNewEntry = [
'class' => $Class,
];
/** @var array $aHistoryEntries */
$aHistoryEntries = appUserPreferences::GetPref(static::USER_PREF_CODE, []);
// Remove same entry from history to avoid duplicates
for($iIdx = 0; $iIdx < count($aHistoryEntries); $iIdx++)
{
if($aHistoryEntries[$iIdx]['class'] === $Class)
{
unset($aHistoryEntries[$iIdx]);
}
}
// Add new entry
array_unshift($aHistoryEntries, $aNewEntry);
// Truncate history
if(count($aHistoryEntries) > static::MAX_HISTORY_SIZE)
{
$aHistoryEntries = array_slice($aHistoryEntries, 0, static::MAX_HISTORY_SIZE);
}
appUserPreferences::SetPref(static::USER_PREF_CODE, $aHistoryEntries);
}
/**
* Return an array of past created object classes
*
* @return array
* @throws \CoreException
* @throws \CoreUnexpectedValue
* @throws \MySQLException
*/
public static function GetLastClasses()
{
$aHistoryEntries = appUserPreferences::GetPref(static::USER_PREF_CODE, []);
for($iIdx = 0; $iIdx < count($aHistoryEntries); $iIdx++)
{
$sClass = $aHistoryEntries[$iIdx]['class'];
// Add class icon
if(!isset($aHistoryEntries[$iIdx]['icon_url']))
{
$sClassIconUrl = MetaModel::GetClassIcon($sClass, false);
// Mind that some classes don't have an icon
if(!empty($sClassIconUrl))
{
$aHistoryEntries[$iIdx]['icon_url'] = $sClassIconUrl;
}
}
// Add class label
if(!isset($aHistoryEntries[$iIdx]['label_html']))
{
$aHistoryEntries[$iIdx]['label_html'] = utils::HtmlEntities(MetaModel::GetName($sClass));
}
// Add url
if(!isset($aHistoryEntries[$iIdx]['target_url']))
{
$aHistoryEntries[$iIdx]['target_url'] = DBObject::ComputeStandardUIPage($sClass).'?operation=new&class='.$sClass;
}
}
return $aHistoryEntries;
}
}

View File

@@ -1,11 +1 @@
<div id="{{ aBreadCrumbs.sId }}" class="ibo-breadcrumbs"></div>
{# TODO: Move this to a dedicated script file #}
{% if aBreadCrumbs.aWidgetOptions is defined %}
<script type="text/javascript" src="../js/components/breadcrumbs.js"></script>
<script type="text/javascript">
setTimeout(function(){
$('#{{ aBreadCrumbs.sId }}').breadcrumbs({{ aBreadCrumbs.aWidgetOptions|json_encode|raw }});
}, 500);
</script>
{% endif %}
<div id="{{ aBreadCrumbs.sId }}" class="ibo-breadcrumbs"></div>

View File

@@ -27,12 +27,4 @@
</div>
</div>
</div>
</div>
{# TODO: Move this to a dedicated script file #}
<script type="text/javascript" src="../js/components/global-search.js"></script>
<script type="text/javascript">
setTimeout(function(){
$('#{{ aGlobalSearch.sId }}').global_search();
}, 500);
</script>
</div>

View File

@@ -0,0 +1,36 @@
<div id="{{ aQuickCreate.sId }}" class="ibo-quick-create" data-role="ibo-quick-create">
<form action="{{ aQuickCreate.sEndpoint }}" method="get" class="ibo-quick-create--head" data-role="ibo-quick-create--head">
<input type="hidden" name="operation" value="new" />
<a href="#" class="ibo-quick-create--icon fas fa-plus" data-role="ibo-quick-create--icon" data-tooltip-content="{{ 'UI:Component:QuickCreate:Tooltip'|dict_s }}" data-tooltip-placement="bottom-start" data-tooltip-distance-offset="25"></a>
<select name="class" class="ibo-quick-create--input" data-role="ibo-quick-create--input" placeholder="{{ 'UI:Component:QuickCreate:Input:Placeholder'|dict_s }}">
<option value="">{{ 'UI:Component:QuickCreate:Input:Placeholder'|dict_s }}</option>
{% for sClassCode, sClassLabel in aQuickCreate.aAvailableClasses %}
<option value="{{ sClassCode }}">{{ sClassLabel }}</option>
{% endfor %}
</select>
</form>
<div class="ibo-quick-create--drawer" data-role="ibo-quick-create--drawer">
<div class="ibo-quick-create--compartment">
<div class="ibo-quick-create--compartment-title" data-role="ibo-quick-create--compartment-title">
<span>{{ 'UI:Component:QuickCreate:Recents:Title'|dict_s }}</span>
</div>
<div class="ibo-quick-create--compartment-content" data-role="ibo-quick-create--compartment-content">
{% if aQuickCreate.aLastClasses|length > 0 %}
{% for aClass in aQuickCreate.aLastClasses %}
<a href="{{ aClass.target_url }}" class="ibo-quick-create--compartment-element" data-role="ibo-quick-create--compartment-element" data-class-code="{{ aQuery.class }}">
{% if aClass.icon_url is defined %}
<img src="{{ aClass.icon_url}}" class="ibo-quick-create--compartment-element-image" />
{% endif %}
{{ aClass.label_html|raw }}
</a>
{% endfor %}
{% else %}
<div class="ibo-quick-create--compartment--placeholder">
<img class="ibo-quick-create--compartment--placeholder-image" src="{{ aPage.sAbsoluteUrlAppRoot }}images/illustrations/undraw-collection/duplicate.svg" />
<div class="ibo-quick-create--compartment--placeholder-hint">{{ 'UI:Component:QuickCreate:LastClasses:NoClass:Placeholder'|dict_s }}</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>

View File

@@ -34,12 +34,4 @@
{% endfor %}
</div>
</div>
</nav>
{# TODO: Move this to a dedicated script file #}
<script type="text/javascript" src="../js/layouts/navigation-menu.js"></script>
<script type="text/javascript">
setTimeout(function(){
$('#{{ aNavigationMenu.sId }}').navigation_menu();
}, 500);
</script>
</nav>

View File

@@ -1,5 +1,6 @@
<nav id="{{ aTopBar.sId }}" class="ibo-top-bar">
<div class="ibo-top-bar--quick-actions">
{{ include('components/quick-create/layout.html.twig', { aQuickCreate: aTopBar.aComponents.aQuickCreate }) }}
{{ include('components/global-search/layout.html.twig', { aGlobalSearch: aTopBar.aComponents.aGlobalSearch }) }}
</div>
{{ include('components/breadcrumbs/layout.html.twig', { aBreadCrumbs: aTopBar.aComponents.aBreadCrumbs }) }}

View File

@@ -30,12 +30,6 @@
</style>
{% endfor %}
{% endblock %}
{% block iboPageJsFiles %}
{% for sJsFile in aPage.aJsFiles %}
<script type="text/javascript" src="{{ sJsFile|add_itop_version }}"></script>
{% endfor %}
{% endblock %}
</head>
<body data-gui-type="backoffice">
{{ include('layouts/navigation-menu/layout.html.twig', { aNavigationMenu: aLayouts.aNavigationMenu }) }}
@@ -46,6 +40,12 @@
</main>
</div>
{% block iboPageJsFiles %}
{% for sJsFile in aPage.aJsFiles %}
<script type="text/javascript" src="{{ sJsFile|add_itop_version }}"></script>
{% endfor %}
{% endblock %}
{% block iboPageJsInlineScripts %}
<script type="text/javascript">
{# TODO: How to do this in native JS? #}
@@ -57,9 +57,11 @@
{% endblock %}
{% block iboPageJsInlineOnDomReady %}
{% for sJsInline in aPage.aJsInlineOnDomReady %}
{{ sJsInline|raw }}
{% endfor %}
setTimeout(function(){
{% for sJsInline in aPage.aJsInlineOnDomReady %}
{{ sJsInline|raw }}
{% endfor %}
}, 50);
{% endblock %}
});
</script>