mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
N°3207 - Global search: Introduce new widget
This commit is contained in:
@@ -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\GlobalSearch\GlobalSearchHelper;
|
||||
use Combodo\iTop\Application\TwigBase\Twig\TwigHelper;
|
||||
|
||||
/**
|
||||
@@ -880,6 +881,7 @@ JS
|
||||
$aData = [
|
||||
'sId' => 'ibo-top-bar',
|
||||
'aComponents' => [
|
||||
'aGlobalSearch' => $this->GetGlobalSearchData(),
|
||||
'aBreadCrumbs' => $this->GetBreadCrumbsData(),
|
||||
],
|
||||
];
|
||||
@@ -887,6 +889,24 @@ JS
|
||||
return $aData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the global search data (last queries)
|
||||
*
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
* @since 2.8.0
|
||||
*/
|
||||
protected function GetGlobalSearchData()
|
||||
{
|
||||
$aData = [
|
||||
'sId' => 'ibo-global-search',
|
||||
'sEndpoint' => utils::GetAbsoluteUrlAppRoot().'pages/UI.php?operation=full_text',
|
||||
'aLastQueries' => GlobalSearchHelper::GetLastQueries(),
|
||||
];
|
||||
|
||||
return $aData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the breadcrumbs data (iTop instance ID, new entry, ...)
|
||||
*
|
||||
|
||||
@@ -17,3 +17,4 @@
|
||||
*/
|
||||
|
||||
@import "breadcrumbs";
|
||||
@import "global-search";
|
||||
|
||||
200
css/backoffice/components/_global-search.scss
Normal file
200
css/backoffice/components/_global-search.scss
Normal file
@@ -0,0 +1,200 @@
|
||||
/*!
|
||||
* 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-global-search--head--background-color: $ibo-color-white-100 !default;
|
||||
|
||||
$ibo-global-search--icon-padding-x: 16px !default;
|
||||
$ibo-global-search--icon-padding-y: 0 !default;
|
||||
|
||||
$ibo-global-search--input--padding: 0 default;
|
||||
$ibo-global-search--input--padding-x--is-opened: 8px !default;
|
||||
$ibo-global-search--input--padding-y--is-opened: 8px !default;
|
||||
$ibo-global-search--input--width: 0 !default;
|
||||
$ibo-global-search--input--width--is-opened: 245px !default;
|
||||
$ibo-global-search--input--text-color: $ibo-color-grey-800 !default;
|
||||
$ibo-global-search--input--placeholder-color: $ibo-color-grey-600 !default;
|
||||
|
||||
$ibo-global-search--drawer--max-height: 200px !default;
|
||||
$ibo-global-search--drawer--padding-x: $ibo-global-search--icon-padding-x !default;
|
||||
$ibo-global-search--drawer--padding-y: 16px !default;
|
||||
$ibo-global-search--drawer--top: -1 * ($ibo-global-search--drawer--max-height) !default;
|
||||
$ibo-global-search--drawer--top--is-opened: 100% !default;
|
||||
$ibo-global-search--drawer--background-color: $ibo-color-white-100 !default;
|
||||
|
||||
$ibo-global-search--compartment-title--margin-bottom: 8px !default;
|
||||
$ibo-global-search--compartment-title--padding-left: 32px !default;
|
||||
$ibo-global-search--compartment-title--text-color: $ibo-color-grey-600 !default;
|
||||
$ibo-global-search--compartment-title--line-spacing: 8px !default;
|
||||
|
||||
$ibo-global-search--compartment-content--text-color: $ibo-color-grey-900 !default;
|
||||
|
||||
$ibo-global-search--compartment-element--margin-bottom: 8px !default;
|
||||
|
||||
$ibo-global-search--compartment-element-image--margin-right: 8px !default;
|
||||
$ibo-global-search--compartment-element-image--width: 20px !default;
|
||||
|
||||
$ibo-global-search--compartment--placeholder-image--margin-top: 24px !default;
|
||||
$ibo-global-search--compartment--placeholder-image--margin-bottom: 16px !default;
|
||||
$ibo-global-search--compartment--placeholder-image--width: 66% !default;
|
||||
|
||||
$ibo-global-search--compartment--placeholder-hint--padding-x: 8px !default;
|
||||
$ibo-global-search--compartment--placeholder-hint--padding-y: 0 !default;
|
||||
$ibo-global-search--compartment--placeholder-hint--text-color: $ibo-color-grey-700 !default;
|
||||
|
||||
/* Animations*/
|
||||
@keyframes ibo-global-search--drawer--opening {
|
||||
from {
|
||||
top: $ibo-global-search--drawer--top;
|
||||
box-shadow: none;
|
||||
}
|
||||
to {
|
||||
top: $ibo-global-search--drawer--top--is-opened;
|
||||
box-shadow: $ibo-elevation-300;
|
||||
}
|
||||
}
|
||||
|
||||
/* SCSS rules */
|
||||
.ibo-global-search{
|
||||
position: relative;
|
||||
@extend %ibo-vertically-centered-content;
|
||||
|
||||
&.ibo-global-search--is-opened{
|
||||
.ibo-global-search--input{
|
||||
padding: $ibo-global-search--input--padding-y--is-opened $ibo-global-search--input--padding-x--is-opened;
|
||||
width: $ibo-global-search--input--width--is-opened;
|
||||
}
|
||||
.ibo-global-search--drawer{
|
||||
animation-name: ibo-global-search--drawer--opening;
|
||||
animation-delay: 0.1s;
|
||||
animation-duration: 0.2s;
|
||||
animation-direction: normal;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ibo-global-search--head{
|
||||
@extend %ibo-vertically-centered-content;
|
||||
background-color: $ibo-global-search--head--background-color;
|
||||
}
|
||||
.ibo-global-search--icon{
|
||||
align-self: center;
|
||||
padding: $ibo-global-search--icon-padding-y $ibo-global-search--icon-padding-x;
|
||||
@extend %ibo-font-ral-nor-400;
|
||||
}
|
||||
.ibo-global-search--input{
|
||||
padding: $ibo-global-search--input--padding;
|
||||
width: $ibo-global-search--input--width;
|
||||
color: $ibo-global-search--input--text-color;
|
||||
@extend %ibo-font-ral-nor-300;
|
||||
|
||||
border: none;
|
||||
outline: none;
|
||||
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&::placeholder{
|
||||
color: $ibo-global-search--input--placeholder-color;
|
||||
}
|
||||
/* This rule is duplicated otherwise Chrome won't be able to parse it. */
|
||||
&:-ms-input-placeholder,
|
||||
&::-ms-input-placeholder{
|
||||
color: $ibo-global-search--input--placeholder-color;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active{
|
||||
@extend .ibo-global-search--input;
|
||||
}
|
||||
}
|
||||
/* TODO: Make drawer appear below the top bar so its shadow is cast on the drawer */
|
||||
.ibo-global-search--drawer{
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: $ibo-global-search--drawer--top;
|
||||
padding: $ibo-global-search--drawer--padding-y $ibo-global-search--drawer--padding-x;
|
||||
background-color: $ibo-global-search--drawer--background-color;
|
||||
box-shadow: none;
|
||||
@extend %ibo-font-ral-nor-100;
|
||||
}
|
||||
.ibo-global-search--compartment-title{
|
||||
margin-bottom: $ibo-global-search--compartment-title--margin-bottom;
|
||||
padding-left: $ibo-global-search--compartment-title--padding-left;
|
||||
overflow-x: hidden;
|
||||
color: $ibo-global-search--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-global-search--compartment-title--text-color;
|
||||
}
|
||||
&::before{
|
||||
right: 100%;
|
||||
margin-right: $ibo-global-search--compartment-title--line-spacing;
|
||||
}
|
||||
&::after{
|
||||
left: 100%;
|
||||
margin-left: $ibo-global-search--compartment-title--line-spacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
.ibo-global-search--compartment-content{
|
||||
color: $ibo-global-search--compartment-content--text-color;
|
||||
}
|
||||
.ibo-global-search--compartment-element{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: inherit;
|
||||
|
||||
@extend %ibo-text-truncated-with-ellipsis;
|
||||
|
||||
&:not(:last-child){
|
||||
margin-bottom: $ibo-global-search--compartment-element--margin-bottom;
|
||||
}
|
||||
}
|
||||
.ibo-global-search--compartment-element-image{
|
||||
margin-right: $ibo-global-search--compartment-element-image--margin-right;
|
||||
width: $ibo-global-search--compartment-element-image--width;
|
||||
}
|
||||
.ibo-global-search--compartment--placeholder{
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ibo-global-search--compartment--placeholder-image{
|
||||
width: $ibo-global-search--compartment--placeholder-image--width;
|
||||
margin-top: $ibo-global-search--compartment--placeholder-image--margin-top;
|
||||
margin-bottom: $ibo-global-search--compartment--placeholder-image--margin-bottom;
|
||||
}
|
||||
.ibo-global-search--compartment--placeholder-hint{
|
||||
text-align: justify;
|
||||
padding: $ibo-global-search--compartment--placeholder-hint--padding-y $ibo-global-search--compartment--placeholder-hint--padding-x;
|
||||
color: $ibo-global-search--compartment--placeholder-hint--text-color;
|
||||
@extend %ibo-font-ral-ita-100;
|
||||
}
|
||||
@@ -48,4 +48,12 @@ $ibo-top-bar--quick-actions--margin-right: $ibo-top-bar--elements-spacing !defau
|
||||
flex-grow: 1; /* Occupy as much width as possible */
|
||||
overflow-x: hidden; /* Avoid glitches when too many items */
|
||||
}
|
||||
}
|
||||
.ibo-top-bar--quick-actions{
|
||||
@extend %ibo-vertically-centered-content;
|
||||
margin-right: var(--ibo-top-bar--quick-actions--margin-right);
|
||||
|
||||
.ibo-global-search{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,8 @@
|
||||
|
||||
// Global search
|
||||
Dict::Add('EN US', 'English', 'English', array(
|
||||
'UI:Component:GlobalSearch:Tooltip' => 'Search throughout the whole application',
|
||||
'UI:Component:GlobalSearch:Input:Placeholder' => 'Search...',
|
||||
'UI:Component:GlobalSearch:Recents:Title' => 'Recents',
|
||||
'UI:Component:GlobalSearch:LastQueries:NoQuery:Placeholder' => 'You haven\'t run any search yet',
|
||||
));
|
||||
1
images/illustrations/undraw-collection/web_search.svg
Normal file
1
images/illustrations/undraw-collection/web_search.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="e8ab07fb-aa66-4eb8-9149-b5abc1630568" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" width="799.70314" height="607.18766" viewBox="0 0 799.70314 607.18766"><rect x="0.27492" y="0.36501" width="643.86161" height="412.35762" fill="#e6e6e6"/><rect x="18.68599" y="52.08494" width="607.03946" height="336.24258" fill="#fff"/><rect width="643.86161" height="27.3536" fill="#6c63ff"/><circle cx="20.327" cy="13.98461" r="5.06978" fill="#fff"/><circle cx="39.57061" cy="13.98461" r="5.06978" fill="#fff"/><circle cx="58.81422" cy="13.98461" r="5.06978" fill="#fff"/><rect x="73.84385" y="86.97284" width="155.98054" height="266.46676" fill="#e6e6e6"/><rect x="256.74961" y="86.97284" width="129.98379" height="73.34799" fill="#6c63ff"/><rect x="256.74961" y="180.74686" width="129.98379" height="78.91873" fill="#e6e6e6"/><rect x="256.74961" y="280.09161" width="129.98379" height="73.34799" fill="#e6e6e6"/><rect x="414.58706" y="86.97284" width="155.98054" height="116.12476" fill="#e6e6e6"/><rect x="414.58706" y="237.31485" width="155.98054" height="116.12476" fill="#6c63ff"/><path d="M936.78726,353.0853c-10.28895-17.5356-30.64482-18.35283-30.64482-18.35283s-19.83563-2.55851-32.56006,24.14841c-11.86019,24.89305-28.22876,48.9278-2.63521,54.75508l4.62294-14.51317,2.863,15.59366a99.28429,99.28429,0,0,0,10.95053.18877c27.40862-.89257,53.5112.26114,52.67074-9.65934C940.93708,392.058,946.6873,369.95805,936.78726,353.0853Z" transform="translate(-200.14843 -146.40617)" fill="#2f2e41"/><path d="M839.21678,446.71189l-9,23-45,42s-27,20-15,26,23-22,23-22l48-29,10-33Z" transform="translate(-200.14843 -146.40617)" fill="#ffb8b8"/><polygon points="654.068 555.306 663.068 578.306 676.068 578.306 665.068 551.306 654.068 555.306" fill="#ffb8b8"/><polygon points="693.068 565.306 702.068 588.306 715.068 588.306 704.068 561.306 693.068 565.306" fill="#ffb8b8"/><path d="M892.21678,715.71189l17-4-22-89,7.30462-68.48066.19538-.01934s12.5-48.5,2.5-61.5-60-8-60-8-19,19-18,62,0,80,7,97,27,62,27,62l16-2-17-74-2.71808-64.32715c2.16314,25.77813,6.51709,74.65881,8.71808,78.32715C861.21678,648.71189,892.21678,715.71189,892.21678,715.71189Z" transform="translate(-200.14843 -146.40617)" fill="#2f2e41"/><path d="M943.21678,467.71189l7,25,31,56s27,24,16,27-28-22-28-22l-35-49-9-36Z" transform="translate(-200.14843 -146.40617)" fill="#ffb8b8"/><circle cx="707.06835" cy="225.30572" r="25" fill="#ffb8b8"/><path d="M894.21678,384.71189l-6,19-10,22,37-15s-2-16,0-20Z" transform="translate(-200.14843 -146.40617)" fill="#ffb8b8"/><path d="M937.21678,424.71189l-23-19-26,11,1.01872-16.226-27.74116,8.85981-4.27756,1.36615-6,31s-4,3-8,20a146.503,146.503,0,0,1-10,29s43-17,64,6c0,0,3-2,2-5s9-18,9-18,0,3,4-3,7-7,7-7Z" transform="translate(-200.14843 -146.40617)" fill="#6c63ff"/><path d="M863.21678,413.71189l-4.3679-3.52126s-3.6321-1.47874-6.6321,3.52126-18,36-18,36l18,9Z" transform="translate(-200.14843 -146.40617)" fill="#6c63ff"/><path d="M923.21678,423.71189l14,1s8,4,8,10,2,38,2,38l-22,7-9-28Z" transform="translate(-200.14843 -146.40617)" fill="#6c63ff"/><path d="M860.327,721.21189l-17,7s-20,1-17,11,33,3,33,3,27,1,27-6-4-15-8-15Z" transform="translate(-200.14843 -146.40617)" fill="#2f2e41"/><path d="M898.327,730.21189l-17,7s-20,1-17,11,33,3,33,3,27,1,27-6-4-15-8-15Z" transform="translate(-200.14843 -146.40617)" fill="#2f2e41"/><polygon points="732.09 204.989 711.979 194.363 684.208 198.71 678.462 224.307 692.765 223.753 696.761 214.348 696.761 223.598 703.361 223.342 707.191 208.37 709.585 224.307 733.048 223.824 732.09 204.989" fill="#2f2e41"/><path d="M797.72843,229.95254a135.01972,135.01972,0,1,0,7.65506,199.40281L971.00163,569.1031a12.44209,12.44209,0,1,0,16.04759-19.01834L821.43108,410.337A135.02731,135.02731,0,0,0,797.72843,229.95254ZM787.05171,396.88021a101.15766,101.15766,0,1,1-12.07656-142.54785A101.15764,101.15764,0,0,1,787.05171,396.88021Z" transform="translate(-200.14843 -146.40617)" fill="#3f3d56"/><path d="M644.50378,408.95686a101.16313,101.16313,0,0,1-17.16557-135.98894q-2.90123,2.92175-5.60938,6.1199A101.15766,101.15766,0,1,0,776.35328,409.55915q2.702-3.20223,5.089-6.559A101.163,101.163,0,0,1,644.50378,408.95686Z" transform="translate(-200.14843 -146.40617)" opacity="0.3"/></svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
110
js/components/global-search.js
Normal file
110
js/components/global-search.js
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.global_search',
|
||||
{
|
||||
// default options
|
||||
options:
|
||||
{
|
||||
|
||||
},
|
||||
css_classes:
|
||||
{
|
||||
opened: 'ibo-global-search--is-opened',
|
||||
},
|
||||
js_selectors:
|
||||
{
|
||||
icon: '[data-role="ibo-global-search--icon"]',
|
||||
form: '[data-role="ibo-global-search--head"]',
|
||||
input: '[data-role="ibo-global-search--input"]',
|
||||
compartment_element: '[data-role="ibo-global-search--compartment-element"]',
|
||||
},
|
||||
|
||||
// the constructor
|
||||
_create: function()
|
||||
{
|
||||
this.element.addClass('ibo-global-search');
|
||||
this._bindEvents();
|
||||
},
|
||||
// events bound via _bind are removed automatically
|
||||
// revert other modifications here
|
||||
_destroy: function()
|
||||
{
|
||||
this.element.removeClass('ibo-global-search');
|
||||
},
|
||||
_bindEvents: function()
|
||||
{
|
||||
const me = this;
|
||||
|
||||
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.compartment_element).on('click', function(oEvent){
|
||||
me._onCompartmentElementClick(oEvent, $(this));
|
||||
});
|
||||
$('body').on('click', function(oEvent){
|
||||
me._onBodyClick(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');
|
||||
},
|
||||
_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();
|
||||
}
|
||||
},
|
||||
_onCompartmentElementClick: function(oEvent, oElementElem)
|
||||
{
|
||||
// Avoid anchor glitch
|
||||
oEvent.preventDefault();
|
||||
|
||||
const sElementQuery = oElementElem.attr('data-query-raw');
|
||||
this.element.find(this.js_selectors.input)
|
||||
.val(sElementQuery)
|
||||
.closest(this.js_selectors.form).trigger('submit');
|
||||
},
|
||||
_onBodyClick: function(oEvent)
|
||||
{
|
||||
if($(oEvent.target.closest('.ibo-global-search')).length === 0)
|
||||
{
|
||||
this.element.removeClass(this.css_classes.opened);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -138,6 +138,7 @@ return array(
|
||||
'CheckStopWatchThresholds' => $baseDir . '/core/ormstopwatch.class.inc.php',
|
||||
'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\\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',
|
||||
|
||||
@@ -368,6 +368,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
|
||||
'CheckStopWatchThresholds' => __DIR__ . '/../..' . '/core/ormstopwatch.class.inc.php',
|
||||
'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\\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',
|
||||
|
||||
22
pages/UI.php
22
pages/UI.php
@@ -17,6 +17,8 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
*/
|
||||
|
||||
use Combodo\iTop\Application\GlobalSearch\GlobalSearchHelper;
|
||||
|
||||
/**
|
||||
* Displays a popup welcome message, once per session at maximum
|
||||
* until the user unchecks the "Display welcome at startup"
|
||||
@@ -546,15 +548,16 @@ try
|
||||
|
||||
case 'full_text': // Global "google-like" search
|
||||
$oP->DisableBreadCrumb();
|
||||
$sFullText = trim(utils::ReadParam('text', '', false, 'raw_data'));
|
||||
$sQuery = trim(utils::ReadPostedParam('query', '', 'raw_data'));
|
||||
$iTune = utils::ReadParam('tune', 0);
|
||||
if (empty($sFullText))
|
||||
if (empty($sQuery))
|
||||
{
|
||||
$oP->p(Dict::S('UI:Search:NoSearch'));
|
||||
}
|
||||
else
|
||||
{
|
||||
$iErrors = 0;
|
||||
$sFullText = $sQuery;
|
||||
|
||||
// Check if a class name/label is supplied to limit the search
|
||||
$sClassName = '';
|
||||
@@ -581,6 +584,21 @@ try
|
||||
$aFullTextNeedles = explode(' ', $sFullText);
|
||||
}
|
||||
|
||||
// Save search to history
|
||||
// - Prepare icon
|
||||
$sQueryIconUrl = null;
|
||||
if(!empty($sClassName))
|
||||
{
|
||||
$sQueryIconUrl = MetaModel::GetClassIcon($sClassName, false);
|
||||
}
|
||||
// - Prepare label
|
||||
$sQueryLabel = null;
|
||||
if($sQuery !== $sFullText)
|
||||
{
|
||||
$sQueryLabel = $sFullText;
|
||||
}
|
||||
GlobalSearchHelper::AddQueryToHistory($sQuery, $sQueryIconUrl, $sQueryLabel);
|
||||
|
||||
// Check the needle length
|
||||
$iMinLenth = MetaModel::GetConfig()->Get('full_text_needle_min');
|
||||
foreach ($aFullTextNeedles as $sNeedle)
|
||||
|
||||
117
sources/application/GlobalSearch/GlobalSearchHelper.php
Normal file
117
sources/application/GlobalSearch/GlobalSearchHelper.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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\GlobalSearch;
|
||||
|
||||
|
||||
use appUserPreferences;
|
||||
use utils;
|
||||
|
||||
/**
|
||||
* Class GlobalSearchHelper
|
||||
*
|
||||
* @author Guillaume Lajarige <guillaume.lajarige@combodo.com>
|
||||
* @package Combodo\iTop\Application\GlobalSearch
|
||||
* @since 2.8.0
|
||||
*/
|
||||
class GlobalSearchHelper
|
||||
{
|
||||
const MAX_HISTORY_SIZE = 10;
|
||||
const USER_PREF_CODE = 'global_search_history';
|
||||
|
||||
/**
|
||||
* Add $sQuery to the history. History is limited to the static::MAX_HISTORY_SIZE last queries.
|
||||
*
|
||||
* @param string $sQuery Raw search query
|
||||
* @param string|null $sIconRelUrl Relative URL of the icon
|
||||
* @param string|null $sLabelAsHtml Alternate label for the query (eg. more human readable or with highlights), MUST be html entities otherwise there can be XSS flaws
|
||||
*
|
||||
* @return void
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public static function AddQueryToHistory($sQuery, $sIconRelUrl = null, $sLabelAsHtml = null)
|
||||
{
|
||||
$aNewQuery = [
|
||||
'query' => $sQuery,
|
||||
];
|
||||
|
||||
// Set icon only when necessary
|
||||
if(!empty($sIconRelUrl))
|
||||
{
|
||||
//Ensure URL is relative to limit space in the preferences and avoid broken links in case app_root_url changes
|
||||
$aNewQuery['icon_url'] = str_replace(utils::GetAbsoluteUrlAppRoot(), '', $sIconRelUrl);
|
||||
}
|
||||
|
||||
// Set label only when necessary to avoid unnecessary space filling of the preferences in the DB
|
||||
if(!empty($sLabelAsHtml))
|
||||
{
|
||||
$aNewQuery['label_html'] = $sLabelAsHtml;
|
||||
}
|
||||
|
||||
/** @var array $aQueriesHistory */
|
||||
$aQueriesHistory = appUserPreferences::GetPref(static::USER_PREF_CODE, []);
|
||||
|
||||
// Remove same query from history to avoid duplicates
|
||||
for($iIdx = 0; $iIdx < count($aQueriesHistory); $iIdx++)
|
||||
{
|
||||
if($aQueriesHistory[$iIdx]['query'] === $sQuery)
|
||||
{
|
||||
unset($aQueriesHistory[$iIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new query
|
||||
array_unshift($aQueriesHistory, $aNewQuery);
|
||||
|
||||
// Truncate history
|
||||
if(count($aQueriesHistory) > static::MAX_HISTORY_SIZE)
|
||||
{
|
||||
$aQueriesHistory = array_slice($aQueriesHistory, 0, static::MAX_HISTORY_SIZE);
|
||||
}
|
||||
|
||||
appUserPreferences::SetPref(static::USER_PREF_CODE, $aQueriesHistory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of pasted queries, including the query itself and its HTML label
|
||||
*
|
||||
* @return array
|
||||
* @throws \CoreException
|
||||
* @throws \CoreUnexpectedValue
|
||||
* @throws \MySQLException
|
||||
*/
|
||||
public static function GetLastQueries()
|
||||
{
|
||||
/** @var array $aLastQueries */
|
||||
$aLastQueries = appUserPreferences::GetPref(static::USER_PREF_CODE, []);
|
||||
|
||||
// Add HTML label if missing
|
||||
for($iIdx = 0; $iIdx < count($aLastQueries); $iIdx++)
|
||||
{
|
||||
if(!isset($aLastQueries[$iIdx]['label_html']))
|
||||
{
|
||||
$aLastQueries[$iIdx]['label_html'] = utils::HtmlEntities($aLastQueries[$iIdx]['query']);
|
||||
}
|
||||
}
|
||||
|
||||
return $aLastQueries;
|
||||
}
|
||||
}
|
||||
38
templates/components/global-search/layout.html.twig
Normal file
38
templates/components/global-search/layout.html.twig
Normal file
@@ -0,0 +1,38 @@
|
||||
<div id="{{ aGlobalSearch.sId }}" class="ibo-global-search" data-role="ibo-global-search">
|
||||
<form action="{{ aGlobalSearch.sEndpoint }}" method="post" class="ibo-global-search--head" data-role="ibo-global-search--head">
|
||||
<a href="#" class="ibo-global-search--icon fas fa-search" data-role="ibo-global-search--icon" data-tooltip-content="{{ 'UI:Component:GlobalSearch:Tooltip'|dict_s }}" data-tooltip-placement="bottom-start" data-tooltip-distance-offset="25"></a>
|
||||
<input type="text" name="query" class="ibo-global-search--input" data-role="ibo-global-search--input" placeholder="{{ 'UI:Component:GlobalSearch:Input:Placeholder'|dict_s }}" />
|
||||
</form>
|
||||
<div class="ibo-global-search--drawer" data-role="ibo-global-search--drawer">
|
||||
<div class="ibo-global-search--compartment">
|
||||
<div class="ibo-global-search--compartment-title" data-role="ibo-global-search--compartment-title">
|
||||
<span>{{ 'UI:Component:GlobalSearch:Recents:Title'|dict_s }}</span>
|
||||
</div>
|
||||
<div class="ibo-global-search--compartment-content" data-role="ibo-global-search--compartment-content">
|
||||
{% if aGlobalSearch.aLastQueries|length > 0 %}
|
||||
{% for aQuery in aGlobalSearch.aLastQueries %}
|
||||
<a href="#" class="ibo-global-search--compartment-element" data-role="ibo-global-search--compartment-element" data-query-raw="{{ aQuery.query }}" title="{{ aQuery.query }}">
|
||||
{% if aQuery.icon_url is defined %}
|
||||
<img src="{{ aPage.sAbsoluteUrlAppRoot ~ aQuery.icon_url}}" class="ibo-global-search--compartment-element-image" />
|
||||
{% endif %}
|
||||
{{ aQuery.label_html|raw }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="ibo-global-search--compartment--placeholder">
|
||||
<img class="ibo-global-search--compartment--placeholder-image" src="{{ aPage.sAbsoluteUrlAppRoot }}images/illustrations/undraw-collection/web_search.svg" />
|
||||
<div class="ibo-global-search--compartment--placeholder-hint">{{ 'UI:Component:GlobalSearch:LastQueries:NoQuery:Placeholder'|dict_s }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</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>
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="ibo-navigation-menu--middle-part">
|
||||
{% for aMenuGroup in aNavigationMenu.aMenuGroups %}
|
||||
{{ include('layouts/navigation-menu/menu-group.html.twig', { aMenuGroup: aMenuGroup }, false) }}
|
||||
{{ include('layouts/navigation-menu/menu-group.html.twig', { aMenuGroup: aMenuGroup }) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="ibo-navigation-menu--bottom-part">
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
<div class="ibo-navigation-menu--menu-groups">
|
||||
{% for aMenuGroup in aNavigationMenu.aMenuGroups %}
|
||||
{{ include('layouts/navigation-menu/menu-nodes.html.twig', { aMenuGroup: aMenuGroup }, false) }}
|
||||
{{ include('layouts/navigation-menu/menu-nodes.html.twig', { aMenuGroup: aMenuGroup }) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% if aMenuNode.aSubMenuNodes is defined and aMenuNode.aSubMenuNodes|length > 0 %}
|
||||
<ul>
|
||||
{% for aSubMenuNode in aMenuNode.aSubMenuNodes %}
|
||||
{{ include('layouts/navigation-menu/menu-node.html.twig', { aMenuNode: aSubMenuNode }, false) }}
|
||||
{{ include('layouts/navigation-menu/menu-node.html.twig', { aMenuNode: aSubMenuNode }) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2 class="ibo-navigation-menu--menu-nodes-title">{{ aMenuGroup.sTitle }}</h2>
|
||||
<ul>
|
||||
{% for aMenuNode in aMenuGroup.aSubMenuNodes %}
|
||||
{{ include('layouts/navigation-menu/menu-node.html.twig', { aMenuNode: aMenuNode }, false) }}
|
||||
{{ include('layouts/navigation-menu/menu-node.html.twig', { aMenuNode: aMenuNode }) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -1,3 +1,6 @@
|
||||
<nav id="{{ aTopBar.sId }}" class="ibo-top-bar">
|
||||
{{ include('components/breadcrumbs/layout.html.twig', { aBreadCrumbs: aTopBar.aComponents.aBreadCrumbs }, false) }}
|
||||
<div class="ibo-top-bar--quick-actions">
|
||||
{{ include('components/global-search/layout.html.twig', { aGlobalSearch: aTopBar.aComponents.aGlobalSearch }) }}
|
||||
</div>
|
||||
{{ include('components/breadcrumbs/layout.html.twig', { aBreadCrumbs: aTopBar.aComponents.aBreadCrumbs }) }}
|
||||
</nav>
|
||||
@@ -38,9 +38,9 @@
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body data-gui-type="backoffice">
|
||||
{{ include('layouts/navigation-menu/layout.html.twig', { aNavigationMenu: aLayouts.aNavigationMenu }, false) }}
|
||||
{{ include('layouts/navigation-menu/layout.html.twig', { aNavigationMenu: aLayouts.aNavigationMenu }) }}
|
||||
<div id="ibo-page-container">
|
||||
{{ include('layouts/top-bar/layout.html.twig', { aTopBar: aLayouts.aTopBar }, false) }}
|
||||
{{ include('layouts/top-bar/layout.html.twig', { aTopBar: aLayouts.aTopBar }) }}
|
||||
<main id="ibo-page-content">
|
||||
{{ aPage.aSanitizedContent|raw }}
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user