public inbox for [email protected]
help / color / mirror / Atom feedFrom: Pramod Ahire <[email protected]>
To: Akshay Joshi <[email protected]>
Cc: pgadmin-hackers <[email protected]>
Subject: Re: Quick search for menu items & help articles
Date: Fri, 15 Jan 2021 12:54:02 +0530
Message-ID: <[email protected]> (raw)
In-Reply-To: <CANxoLDeQtkJjtpG_c8N1t=OS1y3cgPif+X7sLmwg_mq62au1uw@mail.gmail.com>
References: <[email protected]>
<CANxoLDeQtkJjtpG_c8N1t=OS1y3cgPif+X7sLmwg_mq62au1uw@mail.gmail.com>
Hi Akshay,
Thanks for your quick review ! Made changes as per your comments & attached patch with this email.
Pramod Ahire
Software Engineer
C: +91-020-66449600/601
D: +91-9028697679
edbpostgres.com
From: Akshay Joshi <[email protected]>
Date: Friday, 15 January 2021 at 12:14 PM
To: Pramod Ahire <[email protected]>
Cc: pgadmin-hackers <[email protected]>
Subject: Re: Quick search for menu items & help articles
Hi Pramod
Following are the review comments:
linter issue "Undefined variable $black"
In Desktop mode remove the separator beside the search icon.
On Thu, Jan 14, 2021 at 10:18 PM Pramod Ahire <[email protected]> wrote:
Hi Team,
Please find the attached designs & patch that contains complete functionality except below to do for quick search.
To Do:
Unit test cases are not that sufficient to cover complete code, but will be working in background to cover up those one
In pgadmin, for disabled menu items we need to add info that will describe why menu has disabled & how it will be enabled. Either another way to enable all of them & show respective reason in popup that menu is disabled for.
Please do let me know if I missed anything or suggestion of yours.
Thanks !
Pramod Ahire
Software Engineer
C: +91-020-66449600/601
D: +91-9028697679
edbpostgres.com
--
Thanks & Regards
Akshay Joshi
pgAdmin Hacker | Principal Software Architect
EDB Postgres
Mobile: +91 976-788-8246
Attachments:
[application/octet-stream] quick_search_pgadmin_v2.patch (34.3K, 3-quick_search_pgadmin_v2.patch)
download | inline diff:
diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py
index 640a05468..672b3e646 100644
--- a/web/pgadmin/browser/register_browser_preferences.py
+++ b/web/pgadmin/browser/register_browser_preferences.py
@@ -443,6 +443,19 @@ def register_browser_preferences(self):
fields=fields
)
+ self.preference.register(
+ 'keyboard_shortcuts', 'open_quick_search',
+ gettext('Quick Search'), 'keyboardshortcut',
+ {
+ 'alt': False,
+ 'shift': True,
+ 'control': True,
+ 'key': {'key_code': 70, 'char': 'f'}
+ },
+ category_label=PREF_LABEL_KEYBOARD_SHORTCUTS,
+ fields=fields
+ )
+
self.dynamic_tab_title = self.preference.register(
'tab settings', 'dynamic_tabs',
gettext("Dynamic tab size"), 'boolean', False,
diff --git a/web/pgadmin/browser/static/js/keyboard.js b/web/pgadmin/browser/static/js/keyboard.js
index 3381162d0..a69c7009f 100644
--- a/web/pgadmin/browser/static/js/keyboard.js
+++ b/web/pgadmin/browser/static/js/keyboard.js
@@ -44,6 +44,7 @@ _.extend(pgBrowser.keyboardNavigation, {
'drop_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_multiple').value),
'drop_cascade_multiple_objects': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'grid_menu_drop_cascade_multiple').value),
'add_grid_row': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'add_grid_row').value),
+ 'open_quick_search': commonUtils.parseShortcutValue(pgBrowser.get_preference('browser', 'open_quick_search').value),
};
this.shortcutMethods = {
@@ -66,6 +67,7 @@ _.extend(pgBrowser.keyboardNavigation, {
'bindDropMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_multiple_objects}, // Grid Menu Drop Multiple
'bindDropCascadeMultipleObjects': {'shortcuts': this.keyboardShortcut.drop_cascade_multiple_objects}, // Grid Menu Drop Cascade Multiple
'bindAddGridRow': {'shortcuts': this.keyboardShortcut.add_grid_row}, // Subnode Grid Add Row
+ 'bindOpenQuickSearch': {'shortcuts': this.keyboardShortcut.open_quick_search}, // Subnode Grid Refresh Row
};
this.bindShortcuts();
}
@@ -383,6 +385,9 @@ _.extend(pgBrowser.keyboardNavigation, {
return new dialogTabNavigator.dialogTabNavigator(dialogContainer, backward_shortcut, forward_shortcut);
},
+ bindOpenQuickSearch: function() {
+ $('#search_icon').trigger('click');
+ },
});
module.exports = pgAdmin.Browser.keyboardNavigation;
diff --git a/web/pgadmin/browser/static/js/quick_search.js b/web/pgadmin/browser/static/js/quick_search.js
new file mode 100644
index 000000000..82d95dfd4
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search.js
@@ -0,0 +1,28 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {Search} from './quick_search/trigger_search';
+
+// TODO: GUI, Add the logic to show loading screen while fetching result
+const onResultFetch = (url, data) => {
+ // URL can be used for displaying all the result in new page
+ // data will be array of search <name> -> <link>
+ console.warn('URL = ' + url);
+ console.warn(data);
+};
+
+// Entry point - Quick search functionality
+if (document.getElementById('quick-search-component')) {
+ ReactDOM.render(
+ <Search onResult={onResultFetch} />,
+ document.getElementById('quick-search-component')
+ );
+}
diff --git a/web/pgadmin/browser/static/js/quick_search/iframe_component.js b/web/pgadmin/browser/static/js/quick_search/iframe_component.js
new file mode 100644
index 000000000..bf9f8c059
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search/iframe_component.js
@@ -0,0 +1,44 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
+
+// Allow us to render IFrame using React
+// Here we will add the event listener on Iframe load event
+export class Iframe extends Component {
+ static get propTypes() {
+ return {
+ id: PropTypes.string.isRequired,
+ srcURL: PropTypes.string.isRequired,
+ onLoad: PropTypes.func.isRequired,
+ };
+ }
+
+ render () {
+ const iframeStyle = {
+ border: '0',
+ display: 'block',
+ position:'absolute',
+ opacity:'0',
+ };
+ const {id, srcURL, onLoad} = this.props;
+
+ return (
+ <iframe
+ id={id}
+ src={srcURL}
+ onLoad={onLoad}
+ width={'20'}
+ height={'20'}
+ style={iframeStyle}
+ />
+ );
+ }
+}
diff --git a/web/pgadmin/browser/static/js/quick_search/menuitems_help.js b/web/pgadmin/browser/static/js/quick_search/menuitems_help.js
new file mode 100644
index 000000000..b3225129a
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search/menuitems_help.js
@@ -0,0 +1,85 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import gettext from 'sources/gettext';
+
+const LAST_MENU = gettext('About pgAdmin 4');
+
+// Allow us to
+const getMenuName = (item) => {
+ let aLinks = item.getElementsByTagName('a');
+ let name;
+ if (aLinks.length > 0) {
+ name = (aLinks[0].text).trim();
+ name = name.replace(/\.+$/g, '');
+ }
+ return name;
+};
+
+export function menuSearch(param, props) {
+ param = param.trim();
+ const setState = props.setState;
+ let result = [];
+ // Here we will add the matches
+ const parseLI = (_menu, path) => {
+ let _name = getMenuName(_menu);
+ if (_name && _name.toLowerCase().indexOf(param.toLowerCase()) != -1) {
+ let _res = {};
+ _res[_name] = path;
+ _res['element'] = _menu.children[0];
+ result.push(_res);
+ }
+ // Check if last menu then update the parent component's state
+ if (_name === LAST_MENU) {
+ setState(state => ({
+ ...state,
+ fetched: true,
+ data: result,
+ }));
+ }
+ };
+
+ // Recursive function to search in UL
+ const parseUL = (menu, path) => {
+ const menus = Array.from(menu.children);
+ menus.forEach((_menu) => {
+ let _name, _path;
+ if (_menu.tagName == 'UL') {
+ _name = getMenuName(_menu);
+ _path = `${path}/${_name}`;
+ iterItem(_menu, _path);
+ } else if (_menu.tagName == 'LI') {
+ if (_menu.classList.contains('dropdown-submenu')) {
+ _name = getMenuName(_menu);
+ _path = `${path}/${_name}`;
+ iterItem(_menu, _path);
+ } else {
+ parseLI(_menu, path);
+ }
+ }
+ });
+ };
+
+ // Expects LI of menus which contains A & UL
+ const iterItem = (menu, path) => {
+ const subMenus = Array.from(menu.children);
+ subMenus.forEach((_menu) => {
+ if (_menu.tagName == 'UL') {
+ parseUL(_menu, path);
+ }
+ });
+ };
+
+ // Starting Point
+ const navbar = document.querySelector('.navbar-nav');
+ const mainMenus = Array.from(navbar.children);
+
+ mainMenus.forEach((menu) => {
+ iterItem(menu, getMenuName(menu));
+ });
+}
diff --git a/web/pgadmin/browser/static/js/quick_search/online_help.js b/web/pgadmin/browser/static/js/quick_search/online_help.js
new file mode 100644
index 000000000..9808ad007
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search/online_help.js
@@ -0,0 +1,103 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {Iframe} from './iframe_component';
+import url_for from 'sources/url_for';
+
+const extractSearchResult = (list) => {
+ let result = {};
+ for (let idx = 0; idx < list.length; idx++) {
+ let link = list[idx].getElementsByTagName('A');
+ // we are not going to display more than first 10 result as per design
+ if (link.length == 0) {
+ break;
+ }
+ let topicName = link[0].text;
+ let topicLink = url_for('help.static', {
+ 'filename': link[0].getAttribute('href'),
+ });
+ result[topicName] = topicLink;
+ }
+ return result;
+};
+
+export function onlineHelpSearch(param, props) {
+ param = param.split(' ').join('+');
+ const setState = props.setState;
+ const helpURL = url_for('help.static', {
+ 'filename': 'search.html',
+ });
+ const srcURL = `${helpURL}?q=${param}`;
+ let isIFrameLoaded = false;
+ if(document.getElementById('hidden-quick-search-iframe')){
+ document.getElementById('hidden-quick-search-iframe').contentDocument.location.reload(true);
+ }
+
+ // Below function will be called when the page will be loaded in Iframe
+ const _iframeLoaded = () => {
+ if (isIFrameLoaded) {
+ return false;
+ }
+ isIFrameLoaded = true;
+ let iframe = document.getElementById('hidden-quick-search-iframe');
+ let content = (iframe.contentWindow || iframe.contentDocument);
+ let iframeHTML = content.document;
+ window.pooling = setInterval(() => {
+ let resultEl = iframeHTML.getElementById('search-results');
+ let searchResultsH2Tags = resultEl.getElementsByTagName('h2');
+ let list = resultEl && resultEl.getElementsByTagName('LI');
+ let pooling = window.pooling;
+ if ((list && list.length > 0 )) {
+ let res = extractSearchResult(list);
+ // After getting the data, we need to call the Parent component function
+ // which will render the data on the screen
+ if(searchResultsH2Tags[0]['childNodes'][0]['textContent'] != 'Searching'){
+ window.clearInterval(pooling);
+ setState(state => ({
+ ...state,
+ fetched: true,
+ clearedPooling: true,
+ url: srcURL,
+ data: res,
+ }));
+ isIFrameLoaded = false;
+ ReactDOM.unmountComponentAtNode(document.getElementById('quick-search-iframe-container'));
+ }else{
+ setState(state => ({
+ ...state,
+ fetched: true,
+ clearedPooling: false,
+ url: srcURL,
+ data: res,
+ }));
+ }
+
+
+ }else if(searchResultsH2Tags[0]['childNodes'][0]['textContent'] == 'Search Results'){
+ setState(state => ({
+ ...state,
+ fetched: true,
+ clearedPooling: true,
+ url: srcURL,
+ data: {},
+ }));
+ ReactDOM.unmountComponentAtNode(document.getElementById('quick-search-iframe-container'));
+ isIFrameLoaded = false;
+ window.clearInterval(pooling);
+ }
+ }, 500);
+ };
+
+ // Render IFrame
+ ReactDOM.render(
+ <Iframe id='hidden-quick-search-iframe' srcURL={srcURL} onLoad={_iframeLoaded}/>,
+ document.getElementById('quick-search-iframe-container'),
+ );
+}
diff --git a/web/pgadmin/browser/static/js/quick_search/trigger_search.js b/web/pgadmin/browser/static/js/quick_search/trigger_search.js
new file mode 100644
index 000000000..46779ae33
--- /dev/null
+++ b/web/pgadmin/browser/static/js/quick_search/trigger_search.js
@@ -0,0 +1,251 @@
+/////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////
+import React, {useState, useEffect} from 'react';
+import {useDelayDebounce} from 'sources/custom_hooks';
+import {onlineHelpSearch} from './online_help';
+import {menuSearch} from './menuitems_help';
+import $ from 'jquery';
+import gettext from 'sources/gettext';
+
+export function Search() {
+ const [searchTerm, setSearchTerm] = useState('');
+ const [isShowMinLengthMsg, setIsShowMinLengthMsg] = useState(false);
+ let helpLinkTitles = [];
+ let helpLinks = [];
+ const [isMenuLoading, setIsMenuLoading] = useState(false);
+ const [isHelpLoading, setIsHelpLoading] = useState(false);
+ const [menuSearchResult, setMenuSearchResult] = useState({
+ fetched: false,
+ data: [],
+ });
+ const [helpSearchResult, setHelpSearchResult] = useState({
+ fetched: false,
+ clearedPooling: true,
+ url: '',
+ data: [],
+ });
+
+ const [showResults, setShowResults] = useState(false);
+
+ const resetSearchState = () => {
+ setMenuSearchResult(state => ({
+ ...state,
+ fetched: false,
+ data: [],
+ }));
+ setHelpSearchResult(state => ({
+ ...state,
+ fetched: false,
+ clearedPooling: true,
+ url: '',
+ data: {},
+ }));
+ };
+
+ // Below will be called when any changes has been made to state
+ useEffect(() => {
+ helpLinkTitles = Object.keys(helpSearchResult.data);
+ for(let i = 0; i<helpLinkTitles.length;i++){
+ helpLinks.push(<a href={''} target='_blank' rel='noreferrer'>helpLinkTitles[i]</a>);
+ }
+
+ if(menuSearchResult.fetched == true){
+ setIsMenuLoading(false);
+ }
+
+ if(helpSearchResult.fetched == true){
+ setIsHelpLoading(false);
+ }
+ }, [menuSearchResult, helpSearchResult]);
+
+ const initSearch = (param) => {
+ setIsMenuLoading(true);
+ setIsHelpLoading(true);
+
+ onlineHelpSearch(param, {
+ state: helpSearchResult,
+ setState: setHelpSearchResult,
+ });
+ menuSearch(param, {
+ state: menuSearchResult,
+ setState: setMenuSearchResult,
+ });
+ };
+
+
+ // Debounse logic to avoid multiple re-render with each keypress
+ useDelayDebounce(initSearch, searchTerm, 1000);
+
+ const toggleDropdownMenu = () => {
+ let pooling = window.pooling;
+ if(pooling){
+ window.clearInterval(pooling);
+ }
+ document.getElementsByClassName('live-search-field')[0].value = '';
+ setTimeout(function(){
+ document.getElementById('live-search-field').focus();
+ },100);
+ resetSearchState();
+ setShowResults(!showResults);
+ setIsMenuLoading(false);
+ setIsHelpLoading(false);
+ setIsShowMinLengthMsg(false);
+ };
+
+ const refactorMenuItems = (items) => {
+ if(items.length > 0){
+ let menuItemsHtmlElement = [];
+ for(let i=0; i < items.length; i++){
+ Object.keys(items[i]).map( (value) => {
+ if(value != 'element' && value != 'No object selected'){
+ menuItemsHtmlElement.push( <li role='menuitem' key={ 'li-menu-' + i }><a id={ 'li-menu-' + i } href={'#'} className='menu-groups-a dropdown-item' key={ 'menu-' + i } onClick={() => {items[i]['element'].click(); toggleDropdownMenu();}}>
+ {value}
+ <span key={ 'menu-span-' + i }>{refactorPathToMenu(items[i][value])}</span>
+ </a></li>);
+ }
+ });
+ }
+ return menuItemsHtmlElement;
+ }
+ };
+
+ const refactorPathToMenu = (path) => {
+ if(path){
+ let pathArray = path.split('/');
+ let spanElement = [];
+ for(let i = 0; i < pathArray.length; i++ ){
+ if(i == (pathArray.length -1)){
+ spanElement.push(pathArray[i]);
+ }else{
+ spanElement.push(<span key={ 'menu-span-sub' + i }> {pathArray[i]} <i className='fa fa-angle-right' aria-hidden='true'></i> </span>);
+ }
+ }
+ return spanElement;
+ }
+ };
+
+ const onInputValueChange = (value) => {
+ let pooling = window.pooling;
+ if(pooling){
+ window.clearInterval(pooling);
+ }
+ resetSearchState();
+ setSearchTerm('');
+ if(value.length >= 3){
+ setSearchTerm(value);
+ setIsMenuLoading(true);
+ setIsHelpLoading(true);
+ setIsShowMinLengthMsg(false);
+ }else{
+ setIsMenuLoading(false);
+ setIsHelpLoading(false);
+ setIsShowMinLengthMsg(true);
+ }
+ };
+
+ const handleKeyDown = ({ keyCode }) => {
+ if(keyCode == 40){
+ $('a:focus').closest('li').next().find('a').first().focus();
+ return false;
+ }else if(keyCode == 38){
+ $('a:focus').closest('li').prev().find('a').first().focus();
+ return false;
+ }else if(keyCode == 27){
+ toggleDropdownMenu();
+ }
+ };
+
+ const handleEscKeyPress = ({ keyCode }) => {
+ if(keyCode == 27){
+ toggleDropdownMenu();
+ }
+ };
+
+ const handleInputKeyDown = ({ keyCode }) => {
+ if(keyCode == 40){
+ $('#li-menu-0').focus();
+ }
+ };
+
+ return (
+ <div id='quick-search-container' onClick={setSearchTerm}></div>,
+ <div id='quick-search-container' role="menu">
+ <div className='custom-dropdown'>
+ <a href='#'><div tabIndex='0' className='fa fa-search search_icon' id='search_icon' onClick={() => toggleDropdownMenu()} ></div></a>
+
+ <div id='myDropdown' className={showResults ? 'custom-dropdown-content visible' : 'custom-dropdown-content hidden'} onKeyDown={handleEscKeyPress}>
+
+ <input tabIndex='0' autoFocus type='text' autoComplete='off' className='form-control live-search-field' onKeyDown={handleInputKeyDown}
+ aria-label='live-search-field' id='live-search-field' placeholder={gettext('Search')} onChange={(e) => {onInputValueChange(e.target.value);} } />
+
+ <ul onKeyDown={handleKeyDown} style={{marginBottom:0}}>
+ <div className='content-groups' >
+
+ { isShowMinLengthMsg
+ ? (<div className='pad-12 no-results'>
+ <span className='fa fa-info-circle'></span>
+ Please enter minimum 3 characters to search
+ </div>)
+ :''}
+ <div className='fixed-max-height' >
+
+ { (menuSearchResult.fetched == true && isMenuLoading == false ) ?
+ <div>
+ <div className='menu-groups'>
+ <span className='fa fa-window-maximize'></span> {gettext('MENU ITEMS')} ({menuSearchResult.data.length})
+ </div>
+
+
+ {refactorMenuItems(menuSearchResult.data)}
+ </div> : ( (isMenuLoading) ? (<div className='pad-12'><div className="search-icon">{gettext('Searching...')}</div></div>) : '')}
+
+ {(menuSearchResult.data.length == 0 && menuSearchResult.fetched == true && isMenuLoading == false) ? (<div className='pad-12 no-results'><span className='fa fa-info-circle'></span> {gettext('No search results')}</div>):''}
+
+ { (helpSearchResult.fetched == true && isHelpLoading == false) ?
+ <div>
+ <div className='help-groups'>
+ <span className='fa fa-question-circle'></span> {gettext('HELP ARTICLES')} {Object.keys(helpSearchResult.data).length > 10 ?
+ <span>(10 of {Object.keys(helpSearchResult.data).length} )
+ </span>:
+ '(' + Object.keys(helpSearchResult.data).length + ')'}
+ { !helpSearchResult.clearedPooling ? <img src='/static/img/loading.gif' alt={gettext('Loading...')} className='help_loading_icon'/> :''}
+ { Object.keys(helpSearchResult.data).length > 10 ? <a href={helpSearchResult.url} className='pull-right no-padding' target='_blank' rel='noreferrer'>{gettext('Show all')} <span className='fas fa-external-link-alt' ></span></a> : ''}
+ </div>
+
+ {Object.keys(helpSearchResult.data).map( (value, index) => {
+ if(index <= 9) { return <li role='menuitem' key={ 'li-help-' + index }><a tabIndex='0' href={helpSearchResult.data[value]} key={ 'help-' + index } className='dropdown-item' target='_blank' rel='noreferrer'>{value}</a></li>; }
+ })}
+
+ {(Object.keys(helpSearchResult.data).length == 0) ? (<div className='pad-12 no-results'><span className='fa fa-info-circle'></span> {gettext('No search results')}</div>):''}
+ </div> : ( (isHelpLoading && isMenuLoading == false) ? (
+ <div>
+ <div className='help-groups'>
+ <span className='fa fa-question-circle'></span>
+ HELP ARTICLES
+ {Object.keys(helpSearchResult.data).length > 10
+ ? '(10 of ' + Object.keys(helpSearchResult.data).length + ')'
+ : '(' + Object.keys(helpSearchResult.data).length + ')'
+ }
+ { Object.keys(helpSearchResult.data).length > 10
+ ? <a href={helpSearchResult.url} className='pull-right no-padding' target='_blank' rel='noreferrer'>
+ Show all <span className='fas fa-external-link-alt' ></span></a> : ''
+ }
+ </div>
+ <div className='pad-12'><div className="search-icon">{gettext('Searching...')}</div></div>
+ </div>) : '')}
+ </div>
+ </div>
+ </ul>
+ </div>
+ </div>
+ <div id='quick-search-iframe-container' />
+ </div>
+ );
+
+}
diff --git a/web/pgadmin/browser/static/scss/_quick_search.scss b/web/pgadmin/browser/static/scss/_quick_search.scss
new file mode 100644
index 000000000..00c67405b
--- /dev/null
+++ b/web/pgadmin/browser/static/scss/_quick_search.scss
@@ -0,0 +1,138 @@
+#myInput {
+ box-sizing: border-box;
+ background-position: 14px 12px;
+ background-repeat: no-repeat;
+ font-size: 16px;
+ padding: 14px 20px 12px 45px;
+ border: none;
+ border-bottom: 1px solid #ddd;
+}
+
+#myInput:focus {outline: 3px solid #ddd;}
+
+.custom-dropdown {
+ position: relative;
+ display: inline-block;
+}
+
+.custom-dropdown-content {
+ display: none;
+ position: absolute;
+ background-color: $color-bg;
+ min-width: 376px;
+ overflow: auto;
+ border: 1px solid #ddd;
+ z-index: 1;
+ right:0.10em;
+ border-radius: 4px;
+ box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.2);
+}
+
+.custom-dropdown-content a {
+ color: $dropdown-link-color;
+ padding: 6px 12px 6px 21px;
+ text-decoration: none;
+ display: block;
+ cursor:pointer;
+}
+
+.custom-dropdown-content a:hover {
+ color: $black;
+}
+
+#myDropdown a:hover {background-color: $dropdown-link-hover-bg; color:$quick-search-a-text-color ;}
+
+.search_icon{
+ color: $white;
+ cursor: pointer;
+ padding-right: 8px;
+}
+.hidden { display:none; }
+
+.visible { display:block; }
+
+.live-search-field{
+ margin: 6px;
+ width: 360px !important;
+ position: fixed;
+}
+
+.menu-groups, .help-groups{
+ background-color: $color-gray-light;
+ padding: 6px;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.menu-groups .fa, .fas{
+ font-weight:normal !important;
+}
+
+.help-groups .fa, .fas{
+ font-weight:600 !important;
+}
+
+.pad-12{
+ padding:12px;
+}
+
+.no-results{
+ font-size: 14px;
+ color: #697986;
+ text-align: center;
+}
+
+.no-padding{
+ padding:0 !important;
+}
+
+.menu-groups-a{
+ display:flex !important;
+ flex-direction:column;
+}
+
+.menu-groups-a span{
+ font-size: 0.9em;
+ font-weight: 100;
+ color: $quick-search-span-text;
+}
+
+.menu-groups-a:hover span{
+ color: $quick-search-span-text-hover ;
+}
+
+.menu-groups-a:hover span{
+ color: $quick-search-span-text;
+}
+
+.content-groups{
+ margin-top: 3.0em;
+}
+
+.fixed-max-height{
+ max-height: 90vh;
+ overflow-y: auto;
+}
+
+.search-icon{
+ background: $loader-icon-small center center no-repeat;
+ margin: auto !important;
+ height: 22px !important;
+ width: 130px !important;
+ background-position: left !important;
+ font-size: 14px;
+ color: $dropdown-link-color;
+ padding-left: 30px;
+}
+
+.help_loading_icon{
+ height: 16px;
+}
+#myDropdown ul {
+ list-style: none;
+}
+
+.border-right-search-icon{
+ border-right: 2px solid #fff;
+}
+
diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html
index c571df246..b0d4b08bf 100644
--- a/web/pgadmin/browser/templates/browser/index.html
+++ b/web/pgadmin/browser/templates/browser/index.html
@@ -136,7 +136,13 @@ window.onload = function(e){
<ul class="dropdown-menu" role="menu"></ul>
</li>
</ul>
+
+ {% if config.SERVER_MODE != True %}
+ <div id="quick-search-component" ></div>
+ {% endif %}
+
{% if config.SERVER_MODE %}
+ <div id="quick-search-component" class="border-right-search-icon"></div>
<ul class="navbar-nav">
<li class="nav-item active dropdown">
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown"
diff --git a/web/pgadmin/static/img/loading.gif b/web/pgadmin/static/img/loading.gif
new file mode 100644
index 0000000000000000000000000000000000000000..3288d1035d70bb86517e2c233f1a904e41f06b29
GIT binary patch
literal 3208
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
Bt<?Yk
literal 0
HcmV?d00001
diff --git a/web/pgadmin/static/js/custom_hooks.js b/web/pgadmin/static/js/custom_hooks.js
index 9163f0d6c..cc59c1c66 100644
--- a/web/pgadmin/static/js/custom_hooks.js
+++ b/web/pgadmin/static/js/custom_hooks.js
@@ -26,4 +26,15 @@ export function usePrevious(value) {
ref.current = value;
});
return ref.current;
-}
\ No newline at end of file
+}
+
+export function useDelayDebounce(callback, args, delay) {
+ useEffect(() => {
+ const delayDebounceFn = setTimeout(() => {
+ if (args) {
+ callback(args);
+ }
+ }, delay);
+ return () => clearTimeout(delayDebounceFn);
+ }, [args]);
+}
diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss
index fadd71b7b..33f2d108b 100644
--- a/web/pgadmin/static/scss/_pgadmin.style.scss
+++ b/web/pgadmin/static/scss/_pgadmin.style.scss
@@ -1123,3 +1123,9 @@ select:-webkit-autofill:focus {
.pull-left{
float:left
}
+
+.menu-groups-a:hover span, .menu-groups-a:focus span{
+ color: $white !important;
+}
+
+#myDropdown a:hover {background-color: $dropdown-link-hover-bg; color:$white !important;}
diff --git a/web/pgadmin/static/scss/resources/_default.variables.scss b/web/pgadmin/static/scss/resources/_default.variables.scss
index 9d913788d..9a56f1c99 100644
--- a/web/pgadmin/static/scss/resources/_default.variables.scss
+++ b/web/pgadmin/static/scss/resources/_default.variables.scss
@@ -4,6 +4,9 @@ $enable-flex: true;
$white: #fff;
$black: #000;
+$span-text-color: #6B6B6B !default;
+$span-text-color-hover: #6B6B6B !default;
+
$color-bg: $white !default;
$color-fg: #222222 !default;
@@ -349,3 +352,6 @@ $grid-hover-fg-color: $color-fg !default;
$btn-copied-color-fg: $active-color !default;
+$quick-search-a-text-color: $black !default;
+$quick-search-span-text: $span-text-color !default;
+$quick-search-span-text-hover: $span-text-color-hover !default;
diff --git a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
index 129b9a214..12a2b87df 100644
--- a/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
+++ b/web/pgadmin/static/scss/resources/dark/_theme.variables.scss
@@ -121,3 +121,7 @@ $color-success-hover-fg: $color-fg;
$datagrid-selected-color: $color-primary-fg;
$select2-placeholder: #999;
+
+$span-text-color: #9D9FA1 !default;
+$span-text-color-hover: $white !default;
+$quick-search-a-text-color: $white !default;
diff --git a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss
index b895a1223..3885994cc 100644
--- a/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss
+++ b/web/pgadmin/static/scss/resources/high_contrast/_theme.variables.scss
@@ -1,4 +1,5 @@
$white: #FFFFFF;
+$black: #000;
$color-bg: #010B15;
$color-fg: $white;
@@ -202,4 +203,6 @@ $grid-hover-fg-color: #010B15;
$btn-copied-color-fg: #010B15;
-
+$span-text-color: #9D9FA1 !default;
+$span-text-color-hover: $black !default;
+$quick-search-a-text-color: $black !default;
diff --git a/web/regression/javascript/quick_search/quick_search_spec.js b/web/regression/javascript/quick_search/quick_search_spec.js
new file mode 100644
index 000000000..d7f59563f
--- /dev/null
+++ b/web/regression/javascript/quick_search/quick_search_spec.js
@@ -0,0 +1,44 @@
+//////////////////////////////////////////////////////////////////////////
+//
+// pgAdmin 4 - PostgreSQL Tools
+//
+// Copyright (C) 2013 - 2020, The pgAdmin Development Team
+// This software is released under the PostgreSQL Licence
+//
+//////////////////////////////////////////////////////////////////////////
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import { Search } from 'browser/quick_search/trigger_search';
+
+let container;
+
+describe('quick search test cases', function () {
+ beforeEach(() => {
+ container = document.createElement('div');
+ document.body.appendChild(container);
+ act(() => {
+ ReactDOM.render(<Search />, container);
+ });
+ });
+
+ afterEach(() => {
+ document.body.removeChild(container);
+ container = null;
+ });
+
+ it('should have rendered quick-search-container', () => {
+ expect(container.firstChild.id).toEqual('quick-search-container');
+ });
+
+ it('should have 2 childs in quick-search-container', () => {
+ expect(container.firstChild.childNodes.length).toEqual(2);
+ });
+
+ it('element should be html element', () => {
+ let inputElement = document.getElementById('live-search-field');
+ expect(inputElement instanceof HTMLElement).toBeTruthy();
+ });
+
+});
diff --git a/web/webpack.config.js b/web/webpack.config.js
index 96ef708ab..e6655512a 100644
--- a/web/webpack.config.js
+++ b/web/webpack.config.js
@@ -421,6 +421,7 @@ module.exports = [{
use: {
loader: 'imports-loader?' +
'pgadmin.dashboard' +
+ ',pgadmin.browser.quick_search' +
',pgadmin.tools.user_management' +
',pgadmin.browser.object_statistics' +
',pgadmin.browser.dependencies' +
diff --git a/web/webpack.shim.js b/web/webpack.shim.js
index 6fc26021e..bddcda9b8 100644
--- a/web/webpack.shim.js
+++ b/web/webpack.shim.js
@@ -181,6 +181,7 @@ var webpackShimConfig = {
'pgadmin.browser.preferences': path.join(__dirname, './pgadmin/browser/static/js/preferences'),
'pgadmin.browser.menu': path.join(__dirname, './pgadmin/browser/static/js/menu'),
'pgadmin.browser.activity': path.join(__dirname, './pgadmin/browser/static/js/activity'),
+ 'pgadmin.browser.quick_search': path.join(__dirname, './pgadmin/browser/static/js/quick_search'),
'pgadmin.browser.messages': '/browser/js/messages',
'pgadmin.browser.node': path.join(__dirname, './pgadmin/browser/static/js/node'),
'pgadmin.browser.node.ui': path.join(__dirname, './pgadmin/browser/static/js/node.ui'),
[image/png] image001.png (68.4K, 4-image001.png)
download | view image
[image/png] image002.png (68.4K, 5-image002.png)
download | view image
view thread (17+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected]
Subject: Re: Quick search for menu items & help articles
In-Reply-To: <[email protected]>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox