diff --git a/app/assets/javascripts/app/controllers/navigation.coffee b/app/assets/javascripts/app/controllers/navigation.coffee index 705b97ddb..b4a76109e 100644 --- a/app/assets/javascripts/app/controllers/navigation.coffee +++ b/app/assets/javascripts/app/controllers/navigation.coffee @@ -3,24 +3,26 @@ class App.Navigation extends App.ControllerWidgetPermanent elements: '#global-search': 'searchInput' - '#global-search-result': 'searchResult' + '.js-global-search-result': 'searchResult' '.search': 'searchContainer' events: 'click .js-toggleNotifications': 'toggleNotifications' 'click .js-emptySearch': 'emptyAndClose' - 'dblclick .search-holder .icon-magnifier': 'openExtendedSearch' 'submit form.search-holder': 'preventDefault' 'focus #global-search': 'searchFocus' 'blur #global-search': 'searchBlur' 'keydown #global-search': 'listNavigate' - 'click #global-search-result': 'andClose' + 'click .js-global-search-result': 'andClose' 'change .js-menu .js-switch input': 'switch' + 'click .js-details-link': 'openExtendedSearch' constructor: -> super @render() + @throttledSearch = _.throttle @search, 200 + # rerender view, e. g. on langauge change @bind 'ui:rerender', => @renderMenu() @@ -195,7 +197,7 @@ class App.Navigation extends App.ControllerWidgetPermanent @query = '' # reset query cache @searchContainer.addClass('focused') @anyPopoversDestroy() - @searchFunction(0) + @search() searchBlur: (e) => @@ -220,7 +222,7 @@ class App.Navigation extends App.ControllerWidgetPermanent @nudge(e, 1) return else if e.keyCode is 13 # enter - href = @$('#global-search-result .nav-tab.is-hover').attr('href') + href = @$('.global-search-result .nav-tab.is-hover').attr('href') return if !href @locationExecute(href) @emptyAndClose() @@ -228,7 +230,7 @@ class App.Navigation extends App.ControllerWidgetPermanent return # on other keys, show result - @searchFunction(200) + @throttledSearch() nudge: (e, position) => @@ -266,60 +268,57 @@ class App.Navigation extends App.ControllerWidgetPermanent @searchContainer.removeClass('open') @delay(@anyPopoversDestroy, 100, 'removePopovers') - searchFunction: (delay) => + search: => + query = @searchInput.val().trim() + return if !query + return if query is @query + @query = query + @searchContainer.toggleClass('filled', !!@query) - search = => - query = @searchInput.val().trim() - return if !query - return if query is @query - @query = query - @searchContainer.toggleClass('filled', !!@query) + # use cache for search result + if @searchResultCache[@query] + @renderResult(@searchResultCache[@query].result) + currentTime = new Date + return if @searchResultCache[@query].time > currentTime.setSeconds(currentTime.getSeconds() - 20) - # use cache for search result - if @searchResultCache[@query] - @renderResult(@searchResultCache[@query].result) - currentTime = new Date - return if @searchResultCache[@query].time > currentTime.setSeconds(currentTime.getSeconds() - 20) - - App.Ajax.request( - id: 'search' - type: 'GET' - url: "#{@apiPath}/search" - data: - query: @query - processData: true, - success: (data, status, xhr) => - App.Collection.loadAssets(data.assets) - result = {} - for item in data.result - if App[item.type] && App[item.type].find - if !result[item.type] - result[item.type] = [] - item_object = App[item.type].find(item.id) - if item_object.searchResultAttributes - item_object_search_attributes = item_object.searchResultAttributes() - result[item.type].push item_object_search_attributes - else - @log 'error', "No such model #{item.type.toLocaleLowerCase()}.searchResultAttributes()" + App.Ajax.request( + id: 'search' + type: 'GET' + url: "#{@apiPath}/search" + data: + query: @query + processData: true, + success: (data, status, xhr) => + App.Collection.loadAssets(data.assets) + result = {} + for item in data.result + if App[item.type] && App[item.type].find + if !result[item.type] + result[item.type] = [] + item_object = App[item.type].find(item.id) + if item_object.searchResultAttributes + item_object_search_attributes = item_object.searchResultAttributes() + result[item.type].push item_object_search_attributes else - @log 'error', "No such model App.#{item.type}" + @log 'error', "No such model #{item.type.toLocaleLowerCase()}.searchResultAttributes()" + else + @log 'error', "No such model App.#{item.type}" - diff = false - if @searchResultCache[@query] - diff = difference(@searchResultCache[@query].resultRaw, data.result) + diff = false + if @searchResultCache[@query] + diff = difference(@searchResultCache[@query].resultRaw, data.result) - # cache search result - @searchResultCache[@query] = - result: result - resultRaw: data.result - time: new Date + # cache search result + @searchResultCache[@query] = + result: result + resultRaw: data.result + time: new Date - # if result hasn't changed, do not rerender - return if diff isnt false && _.isEmpty(diff) + # if result hasn't changed, do not rerender + return if diff isnt false && _.isEmpty(diff) - @renderResult(result) - ) - @delay(search, delay, 'search') + @renderResult(result) + ) getItems: (data) -> navbar = _.values(data.navbar) @@ -479,7 +478,8 @@ class App.Navigation extends App.ControllerWidgetPermanent e.stopPropagation() @notificationWidget.toggle() - openExtendedSearch: => + openExtendedSearch: (event) -> + event.preventDefault() query = @searchInput.val() @searchInput.val('').blur() if query @@ -487,4 +487,6 @@ class App.Navigation extends App.ControllerWidgetPermanent return @navigate('#search') + + App.Config.set('navigation', App.Navigation, 'Navigations') diff --git a/app/assets/javascripts/app/controllers/search.coffee b/app/assets/javascripts/app/controllers/search.coffee index 66c2222bf..47a2ee434 100644 --- a/app/assets/javascripts/app/controllers/search.coffee +++ b/app/assets/javascripts/app/controllers/search.coffee @@ -4,9 +4,11 @@ class App.Search extends App.Controller '.js-search': 'searchInput' events: + 'click .js-emptySearch': 'empty' 'submit form.search-holder': 'preventDefault' 'keydown .js-search': 'listNavigate' 'click .js-tab': 'showTab' + 'input .js-search': 'updateFilledClass' constructor: -> super @@ -19,18 +21,21 @@ class App.Search extends App.Controller # update taskbar with new meta data App.TaskManager.touch(@task_key) + @throttledSearch = _.throttle @search, 200 + @render() meta: => - title = App.i18n.translateInline('Extended Search') if @query - title += ": #{App.Utils.htmlEscape(@query)}" + title = App.Utils.htmlEscape(@query) + else + title = App.i18n.translateInline('Extended Search') meta = url: @url() id: '' head: title title: title - iconClass: 'magnifier' + iconClass: 'searchdetail' meta url: -> @@ -38,10 +43,9 @@ class App.Search extends App.Controller show: (params) => @navupdate(url: '#search', type: 'menu') - console.log('par', params) return if !params.query @$('.js-search').val(decodeURIComponent(params.query)).trigger('change') - @searchFunction(200, true) + @throttledSearch(true) hide: -> # nothing @@ -79,7 +83,7 @@ class App.Search extends App.Controller ) if @query - @searchFunction(200, true) + @throttledSearch(true) listNavigate: (e) => if e.keyCode is 27 # close on esc @@ -87,71 +91,69 @@ class App.Search extends App.Controller return # on other keys, show result - @searchFunction(200) + @throttledSearch(200) empty: => @searchInput.val('') + @updateFilledClass() # remove not needed popovers @delay(@anyPopoversDestroy, 100, 'removePopovers') - searchFunction: (delay, force = false) => + search: (force = false) => + query = @searchInput.val().trim() + if !force + return if !query + return if query is @query + @query = query - search = => - query = @searchInput.val().trim() - if !force - return if !query - return if query is @query - @query = query + # use cache for search result + if @searchResultCache[@query] + @renderResult(@searchResultCache[@query].result) + currentTime = new Date + return if @searchResultCache[@query].time > currentTime.setSeconds(currentTime.getSeconds() - 20) - # use cache for search result - if @searchResultCache[@query] - @renderResult(@searchResultCache[@query].result) - currentTime = new Date - return if @searchResultCache[@query].time > currentTime.setSeconds(currentTime.getSeconds() - 20) + @updateTask() - @updateTask() - - App.Ajax.request( - id: 'search' - type: 'GET' - url: "#{@apiPath}/search" - data: - query: @query - limit: 200 - processData: true, - success: (data, status, xhr) => - App.Collection.loadAssets(data.assets) - result = {} - for item in data.result - if App[item.type] && App[item.type].find - if !result[item.type] - result[item.type] = [] - item_object = App[item.type].find(item.id) - if item_object.searchResultAttributes - item_object_search_attributes = item_object.searchResultAttributes() - result[item.type].push item_object_search_attributes - else - @log 'error', "No such model #{item.type.toLocaleLowerCase()}.searchResultAttributes()" + App.Ajax.request( + id: 'search' + type: 'GET' + url: "#{@apiPath}/search" + data: + query: @query + limit: 200 + processData: true, + success: (data, status, xhr) => + App.Collection.loadAssets(data.assets) + result = {} + for item in data.result + if App[item.type] && App[item.type].find + if !result[item.type] + result[item.type] = [] + item_object = App[item.type].find(item.id) + if item_object.searchResultAttributes + item_object_search_attributes = item_object.searchResultAttributes() + result[item.type].push item_object_search_attributes else - @log 'error', "No such model App.#{item.type}" + @log 'error', "No such model #{item.type.toLocaleLowerCase()}.searchResultAttributes()" + else + @log 'error', "No such model App.#{item.type}" - diff = false - if @searchResultCache[@query] - diff = difference(@searchResultCache[@query].resultRaw, data.result) + diff = false + if @searchResultCache[@query] + diff = difference(@searchResultCache[@query].resultRaw, data.result) - # cache search result - @searchResultCache[@query] = - result: result - resultRaw: data.result - time: new Date + # cache search result + @searchResultCache[@query] = + result: result + resultRaw: data.result + time: new Date - # if result hasn't changed, do not rerender - return if diff isnt false && _.isEmpty(diff) + # if result hasn't changed, do not rerender + return if diff isnt false && _.isEmpty(diff) - @renderResult(result) - ) - @delay(search, delay, 'search') + @renderResult(result) + ) renderResult: (result = []) => @result = result @@ -161,7 +163,7 @@ class App.Search extends App.Controller count = result[tab.model].length if @model is tab.model @renderTab(tab.model, result[tab.model] || []) - @$(".js-tab#{tab.model} .js-counter").text("(#{count})") + @$(".js-tab#{tab.model} .js-counter").text(count) showTab: (e) => tabs = $(e.currentTarget).closest('.tabs') @@ -213,6 +215,9 @@ class App.Search extends App.Controller App.TaskManager.update(@task_key, { state: current }) App.TaskManager.touch(@task_key) + updateFilledClass: -> + @searchInput.toggleClass 'is-empty', !@searchInput.val() + class Router extends App.ControllerPermanent constructor: (params) -> super diff --git a/app/assets/javascripts/app/views/navigation.jst.eco b/app/assets/javascripts/app/views/navigation.jst.eco index fcf88fdf7..09c734560 100644 --- a/app/assets/javascripts/app/views/navigation.jst.eco +++ b/app/assets/javascripts/app/views/navigation.jst.eco @@ -10,7 +10,18 @@ <%- @Icon('logo') %>
- +
+ + + + + +
diff --git a/app/assets/javascripts/app/views/search/index.jst.eco b/app/assets/javascripts/app/views/search/index.jst.eco index 972b2df11..504e997f3 100644 --- a/app/assets/javascripts/app/views/search/index.jst.eco +++ b/app/assets/javascripts/app/views/search/index.jst.eco @@ -1,19 +1,26 @@ -
+
-
- <%- @Icon('magnifier') %> - " value="<%= @query %>" type="search" autocomplete="off"> - diff --git a/app/assets/stylesheets/svg-dimensions.css b/app/assets/stylesheets/svg-dimensions.css index 6aa729664..e5aae370c 100644 --- a/app/assets/stylesheets/svg-dimensions.css +++ b/app/assets/stylesheets/svg-dimensions.css @@ -70,6 +70,7 @@ .icon-reply-all { width: 16px; height: 16px; } .icon-reply { width: 16px; height: 16px; } .icon-report { width: 20px; height: 20px; } +.icon-searchdetail { width: 18px; height: 14px; } .icon-signout { width: 15px; height: 19px; } .icon-small-dot { width: 16px; height: 16px; } .icon-split { width: 16px; height: 16px; } diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 4f135a673..be766f9c7 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -1800,12 +1800,27 @@ input.has-error { appearance: textfield; border-radius: 19px; padding: 0 17px 0 42px; + + &.is-empty + .empty-search { + visibility: hidden; + } } input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } + + .empty-search { + height: 41px; + width: 50px; + visibility: visible; + + .icon { + position: static; + opacity: 0.5; + } + } } .content { @@ -3038,19 +3053,17 @@ footer { } .nav-tab-icon { - margin-right: 7px; - margin-left: 7px; margin-top: -3px; display: flex; align-items: center; justify-content: center; position: relative; - width: 16px; + width: 30px; } .nav-tab-icon .icon { - width: 16px; - height: 16px; + max-width: 18px; + max-height: 18px; fill: #808080; } @@ -3193,7 +3206,7 @@ footer { cursor: pointer; } - .empty-search .icon-diagonal-cross { + .search .empty-search .icon-diagonal-cross { fill: white; opacity: 0.5; } @@ -3272,10 +3285,7 @@ footer { display: none; } - .search .custom-dropdown-menu { - margin: 0; - padding: 0; - list-style: none; + .global-search-menu { background: #26272e; position: absolute; left: 0; @@ -3285,16 +3295,47 @@ footer { z-index: 900; display: none; overflow: auto; + + .divider { + height: 1px; + background: #2f3238; + margin: 14px 0 17px; + } } - .search.open .custom-dropdown-menu { + .search.open .global-search-menu { display: block; } - .search .custom-dropdown-menu .divider { - height: 1px; - background: #2f3238; - margin: 14px 0 17px; + .global-search-detail-link { + padding: 9px 15px 8px 0; + margin-bottom: 7px; + height: auto !important; + + .nav-tab-icon { + width: 18px; + margin-left: 10px; + margin-right: 10px; + + .icon { + width: 18px; + height: 14px; + } + } + + .nav-tab-name { + .icon { + fill: currentColor; + margin: -2px 0 0 3px; + vertical-align: middle; + } + } + } + + .global-search-result { + margin: 0; + padding: 0; + list-style: none; } .user-menu { @@ -8218,6 +8259,10 @@ output { } } +.detail-search-header { + margin: 20px 0 32px; +} + /* ---------------- diff --git a/contrib/icon-sprite.sketch b/contrib/icon-sprite.sketch index f06a7b0df..39f61ed67 100644 Binary files a/contrib/icon-sprite.sketch and b/contrib/icon-sprite.sketch differ diff --git a/public/assets/images/icons.svg b/public/assets/images/icons.svg index e6f2a902a..8e2b044f4 100644 --- a/public/assets/images/icons.svg +++ b/public/assets/images/icons.svg @@ -1 +1 @@ -arrow-downarrow-leftarrow-rightarrow-upchatcheckbox-checkedcheckboxcheckmarkclipboardclockcloudcogcrowndashboarddiagonal-crossdownloaddraggabledropdown-listemail-buttonemaileyedropperfacebook-buttonfacebookformgoogle-buttongrouphelpimportantin-processinfoline-left-arrowline-right-arrowlinkedin-buttonlistloadinglock-openlocklogotypelong-arrow-rightmagnifiermarkermessageminus-smallminusmood-badmood-goodmood-okmood-super-badmood-supergoodmutenoteone-ticketorganizationoutbound-callsoverviewspackagepaperclippenpersonphoneplus-smallplusradio-checkedradioreceived-callsreloadreopeningreply-allreplyreportsignoutsmall-dotsplitstatus-modified-outer-circlestatusstopwatchswitchViewtask-stateteamtemplatestoolstotal-ticketstrashtwitter-buttontwitterunmuteuserwebzoom-inzoom-out \ No newline at end of file +arrow-downarrow-leftarrow-rightarrow-upchatcheckbox-checkedcheckboxcheckmarkclipboardclockcloudcogcrowndashboarddiagonal-crossdownloaddraggabledropdown-listemail-buttonemaileyedropperfacebook-buttonfacebookformgoogle-buttongrouphelpimportantin-processinfoline-left-arrowline-right-arrowlinkedin-buttonlistloadinglock-openlocklogotypelong-arrow-rightmagnifiermarkermessageminus-smallminusmood-badmood-goodmood-okmood-super-badmood-supergoodmutenoteone-ticketorganizationoutbound-callsoverviewspackagepaperclippenpersonphoneplus-smallplusradio-checkedradioreceived-callsreloadreopeningreply-allreplyreportsearchdetailsignoutsmall-dotsplitstatus-modified-outer-circlestatusstopwatchswitchViewtask-stateteamtemplatestoolstotal-ticketstrashtwitter-buttontwitterunmuteuserwebzoom-inzoom-out \ No newline at end of file diff --git a/public/assets/images/icons/searchdetail.svg b/public/assets/images/icons/searchdetail.svg new file mode 100644 index 000000000..3cbcdacbe --- /dev/null +++ b/public/assets/images/icons/searchdetail.svg @@ -0,0 +1,13 @@ + + + + searchdetail + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index f8d99d748..9d7c1a8cf 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -2297,7 +2297,7 @@ wait untill text in selector disabppears # open ticket #instance.find_element(partial_link_text: params[:number] } ).click - instance.execute_script("$(\"#global-search-result a:contains('#{params[:value]}') .nav-tab-icon\").click()") + instance.execute_script("$(\".js-global-search-result a:contains('#{params[:value]}') .nav-tab-icon\").click()") sleep 1 number = instance.find_elements(css: '.active .ticketZoom-header .ticket-number')[0].text if number !~ /#{params[:number]}/ @@ -2332,7 +2332,7 @@ wait untill text in selector disabppears # open ticket #instance.find_element(partial_link_text: params[:title] } ).click - instance.execute_script("$(\"#global-search-result a:contains('#{params[:title]}') .nav-tab-icon\").click()") + instance.execute_script("$(\".js-global-search-result a:contains('#{params[:title]}') .nav-tab-icon\").click()") sleep 1 title = instance.find_elements(css: '.active .ticketZoom-header .js-objectTitle')[0].text if title !~ /#{params[:title]}/ @@ -2420,7 +2420,7 @@ wait untill text in selector disabppears element.send_keys(params[:value]) sleep 2 #instance.find_element(partial_link_text: params[:value] } ).click - instance.execute_script("$(\"#global-search-result a:contains('#{params[:value]}') .nav-tab-icon\").click()") + instance.execute_script("$(\".js-global-search-result a:contains('#{params[:value]}') .nav-tab-icon\").click()") sleep 1 name = instance.find_elements(css: '.active h1')[0].text if name !~ /#{params[:value]}/ @@ -2453,7 +2453,7 @@ wait untill text in selector disabppears element.send_keys(params[:value]) sleep 3 #instance.find_element(partial_link_text: params[:value]).click - instance.execute_script("$(\"#global-search-result a:contains('#{params[:value]}') .nav-tab-icon\").click()") + instance.execute_script("$(\".js-global-search-result a:contains('#{params[:value]}') .nav-tab-icon\").click()") sleep 1 name = instance.find_elements(css: '.active h1')[0].text if name !~ /#{params[:value]}/