From d4694b271f189a8ef14366a945f0c47dc3a1fd98 Mon Sep 17 00:00:00 2001 From: Molkobain Date: Mon, 27 Jul 2020 15:07:40 +0200 Subject: [PATCH] =?UTF-8?q?N=C2=B02847=20-=20Rework=20iTopWebPage=20layout?= =?UTF-8?q?=20(WIP=20Part=20IV)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- application/itopwebpage.class.inc.php | 123 +++++++--- css/backoffice/components/_all.scss | 1 + css/backoffice/components/_quick-create.scss | 222 ++++++++++++++++++ ...hp => en.dictionary.itop.quick-create.php} | 6 +- .../undraw-collection/duplicate.svg | 1 + js/components/global-search.js | 59 ++++- js/components/quick-create.js | 166 +++++++++++++ js/layouts/navigation-menu.js | 8 +- lib/composer/autoload_classmap.php | 1 + lib/composer/autoload_static.php | 1 + pages/UI.php | 2 + .../QuickCreate/QuickCreateHelper.php | 123 ++++++++++ .../components/breadcrumbs/layout.html.twig | 12 +- .../components/global-search/layout.html.twig | 10 +- .../components/quick-create/layout.html.twig | 36 +++ .../layouts/navigation-menu/layout.html.twig | 10 +- templates/layouts/top-bar/layout.html.twig | 1 + templates/pages/backoffice/layout.html.twig | 20 +- 18 files changed, 726 insertions(+), 76 deletions(-) create mode 100644 css/backoffice/components/_quick-create.scss rename dictionaries/ui/components/{en.dictionary.itop.global-create.php => en.dictionary.itop.quick-create.php} (69%) create mode 100644 images/illustrations/undraw-collection/duplicate.svg create mode 100644 js/components/quick-create.js create mode 100644 sources/application/QuickCreate/QuickCreateHelper.php create mode 100644 templates/components/quick-create/layout.html.twig diff --git a/application/itopwebpage.class.inc.php b/application/itopwebpage.class.inc.php index 3d952b4be..908b0ade7 100644 --- a/application/itopwebpage.class.inc.php +++ b/application/itopwebpage.class.inc.php @@ -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 = -// <<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( <<add_ready_script( <<add_ready_script( << '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(<< '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(<< 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(<< '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(<<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'; diff --git a/css/backoffice/components/_all.scss b/css/backoffice/components/_all.scss index 784446164..a33528c15 100644 --- a/css/backoffice/components/_all.scss +++ b/css/backoffice/components/_all.scss @@ -17,4 +17,5 @@ */ @import "breadcrumbs"; +@import "quick-create"; @import "global-search"; diff --git a/css/backoffice/components/_quick-create.scss b/css/backoffice/components/_quick-create.scss new file mode 100644 index 000000000..e2496fb37 --- /dev/null +++ b/css/backoffice/components/_quick-create.scss @@ -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; +} \ No newline at end of file diff --git a/dictionaries/ui/components/en.dictionary.itop.global-create.php b/dictionaries/ui/components/en.dictionary.itop.quick-create.php similarity index 69% rename from dictionaries/ui/components/en.dictionary.itop.global-create.php rename to dictionaries/ui/components/en.dictionary.itop.quick-create.php index a0a5404de..27fa215d2 100644 --- a/dictionaries/ui/components/en.dictionary.itop.global-create.php +++ b/dictionaries/ui/components/en.dictionary.itop.quick-create.php @@ -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', )); \ No newline at end of file diff --git a/images/illustrations/undraw-collection/duplicate.svg b/images/illustrations/undraw-collection/duplicate.svg new file mode 100644 index 000000000..8f90376aa --- /dev/null +++ b/images/illustrations/undraw-collection/duplicate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/components/global-search.js b/js/components/global-search.js index 7ef2c7618..3fbd63219 100644 --- a/js/components/global-search.js +++ b/js/components/global-search.js @@ -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'); } }); }); diff --git a/js/components/quick-create.js b/js/components/quick-create.js new file mode 100644 index 000000000..eedc9a647 --- /dev/null +++ b/js/components/quick-create.js @@ -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'); + } + }); +}); diff --git a/js/layouts/navigation-menu.js b/js/layouts/navigation-menu.js index 54551e6cc..0baf1b24d 100644 --- a/js/layouts/navigation-menu.js +++ b/js/layouts/navigation-menu.js @@ -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 diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index b4c456415..23a433436 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -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', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 8702ad4f2..ab1d94aec 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -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', diff --git a/pages/UI.php b/pages/UI.php index 52ef9c1e1..9faddda1b 100644 --- a/pages/UI.php +++ b/pages/UI.php @@ -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*/); diff --git a/sources/application/QuickCreate/QuickCreateHelper.php b/sources/application/QuickCreate/QuickCreateHelper.php new file mode 100644 index 000000000..28711df65 --- /dev/null +++ b/sources/application/QuickCreate/QuickCreateHelper.php @@ -0,0 +1,123 @@ + + * @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; + } +} \ No newline at end of file diff --git a/templates/components/breadcrumbs/layout.html.twig b/templates/components/breadcrumbs/layout.html.twig index c9f3a4cc8..9a287c1bd 100644 --- a/templates/components/breadcrumbs/layout.html.twig +++ b/templates/components/breadcrumbs/layout.html.twig @@ -1,11 +1 @@ -
- -{# TODO: Move this to a dedicated script file #} -{% if aBreadCrumbs.aWidgetOptions is defined %} - - -{% endif %} \ No newline at end of file +
\ No newline at end of file diff --git a/templates/components/global-search/layout.html.twig b/templates/components/global-search/layout.html.twig index 3a10e770c..e545f7635 100644 --- a/templates/components/global-search/layout.html.twig +++ b/templates/components/global-search/layout.html.twig @@ -27,12 +27,4 @@ - - -{# TODO: Move this to a dedicated script file #} - - \ No newline at end of file + \ No newline at end of file diff --git a/templates/components/quick-create/layout.html.twig b/templates/components/quick-create/layout.html.twig new file mode 100644 index 000000000..62e180fd2 --- /dev/null +++ b/templates/components/quick-create/layout.html.twig @@ -0,0 +1,36 @@ +
+
+ + + +
+
+
+
+ {{ 'UI:Component:QuickCreate:Recents:Title'|dict_s }} +
+
+ {% if aQuickCreate.aLastClasses|length > 0 %} + {% for aClass in aQuickCreate.aLastClasses %} + + {% if aClass.icon_url is defined %} + + {% endif %} + {{ aClass.label_html|raw }} + + {% endfor %} + {% else %} +
+ +
{{ 'UI:Component:QuickCreate:LastClasses:NoClass:Placeholder'|dict_s }}
+
+ {% endif %} +
+
+
+
\ No newline at end of file diff --git a/templates/layouts/navigation-menu/layout.html.twig b/templates/layouts/navigation-menu/layout.html.twig index d199c7b0d..869dbe3e8 100644 --- a/templates/layouts/navigation-menu/layout.html.twig +++ b/templates/layouts/navigation-menu/layout.html.twig @@ -34,12 +34,4 @@ {% endfor %} - - -{# TODO: Move this to a dedicated script file #} - - \ No newline at end of file + \ No newline at end of file diff --git a/templates/layouts/top-bar/layout.html.twig b/templates/layouts/top-bar/layout.html.twig index 105e30c39..37d2fe28a 100644 --- a/templates/layouts/top-bar/layout.html.twig +++ b/templates/layouts/top-bar/layout.html.twig @@ -1,5 +1,6 @@