From b11a042d089c55e7b9326270dfb2b1937eb5ae5d Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 20 Jan 2015 16:39:07 +0100 Subject: [PATCH] SONAR-6041 New webapp layout --- server/sonar-web/Gruntfile.coffee | 9 + .../sonar-web/src/main/hbs/nav/_nav-logo.hbs | 18 ++ .../src/main/hbs/nav/_nav-navbar-label.hbs | 1 + .../src/main/hbs/nav/nav-context-navbar.hbs | 38 ++++ .../src/main/hbs/nav/nav-global-navbar.hbs | 63 ++++++ .../src/main/hbs/nav/nav-search-empty.hbs | 1 + .../src/main/hbs/nav/nav-search-item.hbs | 16 ++ .../sonar-web/src/main/hbs/nav/nav-search.hbs | 6 + server/sonar-web/src/main/js/application.js | 23 +- .../js/common/selectable-collection-view.js | 62 ++++++ server/sonar-web/src/main/js/nav/app.js | 34 +++ .../src/main/js/nav/context-navbar-view.js | 17 ++ .../src/main/js/nav/global-navbar-view.js | 83 +++++++ .../sonar-web/src/main/js/nav/search-view.js | 154 +++++++++++++ .../main/js/third-party/bootstrap/dropdown.js | 161 ++++++++++++++ .../sonar-web/src/main/less/components.less | 2 + .../src/main/less/components/dropdowns.less | 119 ++++++++++ .../src/main/less/components/menu.less | 53 +++++ .../less/components/search-navigator.less | 2 +- server/sonar-web/src/main/less/dashboard.less | 1 + server/sonar-web/src/main/less/layout.less | 6 +- server/sonar-web/src/main/less/navbar.less | 183 +++++++++++++++ server/sonar-web/src/main/less/style.less | 48 ---- server/sonar-web/src/main/less/ui.less | 58 +++-- server/sonar-web/src/main/less/variables.less | 12 + .../controllers/api/components_controller.rb | 4 +- .../WEB-INF/app/views/account/index.html.erb | 2 +- .../app/views/comparison/index.html.erb | 2 +- .../app/views/dependencies/index.html.erb | 126 +++++------ .../app/views/layouts/_breadcrumb.html.erb | 104 +-------- .../app/views/layouts/_layout.html.erb | 182 +-------------- .../app/views/layouts/_navbar.html.erb | 5 + .../app/views/layouts/_navbar_conf.html.erb | 29 +++ .../layouts/_navbar_conf_context.html.erb | 208 ++++++++++++++++++ .../layouts/_navbar_conf_global.html.erb | 75 +++++++ .../layouts/_navbar_conf_settings.html.erb | 109 +++++++++ .../views/layouts/_recent_history.html.erb | 9 + .../app/views/layouts/application.html.erb | 3 +- .../WEB-INF/app/views/settings/index.html.erb | 4 +- 39 files changed, 1607 insertions(+), 425 deletions(-) create mode 100644 server/sonar-web/src/main/hbs/nav/_nav-logo.hbs create mode 100644 server/sonar-web/src/main/hbs/nav/_nav-navbar-label.hbs create mode 100644 server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs create mode 100644 server/sonar-web/src/main/hbs/nav/nav-global-navbar.hbs create mode 100644 server/sonar-web/src/main/hbs/nav/nav-search-empty.hbs create mode 100644 server/sonar-web/src/main/hbs/nav/nav-search-item.hbs create mode 100644 server/sonar-web/src/main/hbs/nav/nav-search.hbs create mode 100644 server/sonar-web/src/main/js/common/selectable-collection-view.js create mode 100644 server/sonar-web/src/main/js/nav/app.js create mode 100644 server/sonar-web/src/main/js/nav/context-navbar-view.js create mode 100644 server/sonar-web/src/main/js/nav/global-navbar-view.js create mode 100644 server/sonar-web/src/main/js/nav/search-view.js create mode 100644 server/sonar-web/src/main/js/third-party/bootstrap/dropdown.js create mode 100644 server/sonar-web/src/main/less/components/dropdowns.less create mode 100644 server/sonar-web/src/main/less/components/menu.less create mode 100644 server/sonar-web/src/main/less/navbar.less create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_navbar.html.erb create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_navbar_conf.html.erb create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_navbar_conf_context.html.erb create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_navbar_conf_global.html.erb create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_navbar_conf_settings.html.erb create mode 100644 server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_recent_history.html.erb diff --git a/server/sonar-web/Gruntfile.coffee b/server/sonar-web/Gruntfile.coffee index 60b9b6b3e87..2ce259390f6 100644 --- a/server/sonar-web/Gruntfile.coffee +++ b/server/sonar-web/Gruntfile.coffee @@ -91,6 +91,7 @@ module.exports = (grunt) -> '<%= pkg.assets %>js/third-party/numeral.js' '<%= pkg.assets %>js/third-party/numeral-languages.js' '<%= pkg.assets %>js/third-party/bootstrap/tooltip.js' + '<%= pkg.assets %>js/third-party/bootstrap/dropdown.js' '<%= pkg.assets %>js/select2-jquery-ui-fix.js' '<%= pkg.assets %>js/widgets/base.js' '<%= pkg.assets %>js/widgets/widget.js' @@ -134,6 +135,7 @@ module.exports = (grunt) -> '<%= pkg.assets %>js/third-party/numeral.js' '<%= pkg.assets %>js/third-party/numeral-languages.js' '<%= pkg.assets %>js/third-party/bootstrap/tooltip.js' + '<%= pkg.assets %>js/third-party/bootstrap/dropdown.js' '<%= pkg.assets %>js/select2-jquery-ui-fix.js' '<%= pkg.assets %>js/widgets/base.js' '<%= pkg.assets %>js/widgets/widget.js' @@ -235,6 +237,10 @@ module.exports = (grunt) -> name: 'analysis-reports/app' out: '<%= pkg.assets %>build/js/analysis-reports/app.js' + nav: options: + name: 'nav/app' + out: '<%= pkg.assets %>build/js/nav/app.js' + handlebars: options: @@ -285,6 +291,9 @@ module.exports = (grunt) -> '<%= pkg.assets %>js/templates/analysis-reports.js': [ '<%= pkg.sources %>hbs/analysis-reports/**/*.hbs' ] + '<%= pkg.assets %>js/templates/nav.js': [ + '<%= pkg.sources %>hbs/nav/**/*.hbs' + ] clean: diff --git a/server/sonar-web/src/main/hbs/nav/_nav-logo.hbs b/server/sonar-web/src/main/hbs/nav/_nav-logo.hbs new file mode 100644 index 00000000000..fcb273a09ae --- /dev/null +++ b/server/sonar-web/src/main/hbs/nav/_nav-logo.hbs @@ -0,0 +1,18 @@ + diff --git a/server/sonar-web/src/main/hbs/nav/_nav-navbar-label.hbs b/server/sonar-web/src/main/hbs/nav/_nav-navbar-label.hbs new file mode 100644 index 00000000000..df44ac06af2 --- /dev/null +++ b/server/sonar-web/src/main/hbs/nav/_nav-navbar-label.hbs @@ -0,0 +1 @@ +{{#if labelLocalized}}{{labelLocalized}}{{else}}{{t label}}{{/if}} diff --git a/server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs b/server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs new file mode 100644 index 00000000000..df091ff766e --- /dev/null +++ b/server/sonar-web/src/main/hbs/nav/nav-context-navbar.hbs @@ -0,0 +1,38 @@ +
+ + + +
diff --git a/server/sonar-web/src/main/hbs/nav/nav-global-navbar.hbs b/server/sonar-web/src/main/hbs/nav/nav-global-navbar.hbs new file mode 100644 index 00000000000..5b000361c48 --- /dev/null +++ b/server/sonar-web/src/main/hbs/nav/nav-global-navbar.hbs @@ -0,0 +1,63 @@ +
+ + + + +
diff --git a/server/sonar-web/src/main/hbs/nav/nav-search-empty.hbs b/server/sonar-web/src/main/hbs/nav/nav-search-empty.hbs new file mode 100644 index 00000000000..ccac4831661 --- /dev/null +++ b/server/sonar-web/src/main/hbs/nav/nav-search-empty.hbs @@ -0,0 +1 @@ +{{t 'no_results'}} diff --git a/server/sonar-web/src/main/hbs/nav/nav-search-item.hbs b/server/sonar-web/src/main/hbs/nav/nav-search-item.hbs new file mode 100644 index 00000000000..1de73f23eab --- /dev/null +++ b/server/sonar-web/src/main/hbs/nav/nav-search-item.hbs @@ -0,0 +1,16 @@ + + {{#if extra}} + {{extra}} + {{/if}} + {{#if q}}{{qualifierIcon q}}{{/if}} + {{#if subtitle}} + {{title}} +
+ {{#if extra}} +   + {{/if}} + {{subtitle}} + {{else}} + {{name}} + {{/if}} +
diff --git a/server/sonar-web/src/main/hbs/nav/nav-search.hbs b/server/sonar-web/src/main/hbs/nav/nav-search.hbs new file mode 100644 index 00000000000..ed298aef314 --- /dev/null +++ b/server/sonar-web/src/main/hbs/nav/nav-search.hbs @@ -0,0 +1,6 @@ + + + + +
diff --git a/server/sonar-web/src/main/js/application.js b/server/sonar-web/src/main/js/application.js index a1c209a82b6..5b15d75608f 100644 --- a/server/sonar-web/src/main/js/application.js +++ b/server/sonar-web/src/main/js/application.js @@ -21,13 +21,13 @@ function toggleFav(resourceId, elt) { }}); } -function dashboardParameters() { +function dashboardParameters (urlHasSomething) { var queryString = window.location.search; - var parameters = ''; + var parameters = []; var matchDashboard = queryString.match(/did=\d+/); if (matchDashboard && $j('#is-project-dashboard').length === 1) { - parameters += (matchDashboard[0] + '&'); + parameters.push(matchDashboard[0]); } var matchPeriod = queryString.match(/period=\d+/); @@ -35,14 +35,15 @@ function dashboardParameters() { // If we have a match for period, check that it is not project-specific var period = parseInt(/period=(\d+)/.exec(queryString)[1]); if (period <= 3) { - parameters += matchPeriod[0] + '&'; + parameters.push(matchPeriod[0]); } } - if (parameters !== '') { - parameters = '?' + parameters; + var query = parameters.join('&'); + if (query !== '') { + query = (urlHasSomething ? '&' : '?') + query; } - return parameters; + return query; } @@ -90,7 +91,7 @@ Treemap.prototype.load = function () { $j.ajax({ type: 'GET', url: baseUrl + '/treemap/index?html_id=' + this.id + '&size_metric=' + this.sizeMetric + - '&color_metric=' + this.colorMetric + '&resource=' + context.rid, + '&color_metric=' + this.colorMetric + '&resource=' + context.rid, dataType: 'html', success: function (data) { if (data.length > 1) { @@ -340,11 +341,7 @@ jQuery(function () { // Define global shortcuts key('s', function () { - jQuery('#searchInput').focus().on('keydown', function (e) { - if (e.keyCode === 27) { - jQuery('#searchInput').blur(); - } - }); + jQuery('.js-search-dropdown-toggle').dropdown('toggle'); return false; }); }); diff --git a/server/sonar-web/src/main/js/common/selectable-collection-view.js b/server/sonar-web/src/main/js/common/selectable-collection-view.js new file mode 100644 index 00000000000..6ddce02f574 --- /dev/null +++ b/server/sonar-web/src/main/js/common/selectable-collection-view.js @@ -0,0 +1,62 @@ +define(function () { + + return Marionette.CollectionView.extend({ + + initialize: function () { + this.resetSelectedIndex(); + this.listenTo(this.collection, 'reset', this.resetSelectedIndex); + }, + + resetSelectedIndex: function () { + this.selectedIndex = 0; + }, + + onRender: function () { + this.selectCurrent(); + }, + + submitCurrent: function () { + var view = this.children.findByIndex(this.selectedIndex); + if (view != null) { + view.submit(); + } + }, + + selectCurrent: function () { + this.selectItem(this.selectedIndex); + }, + + selectNext: function () { + if (this.selectedIndex < this.collection.length - 1) { + this.deselectItem(this.selectedIndex); + this.selectedIndex++; + this.selectItem(this.selectedIndex); + } + }, + + selectPrev: function () { + if (this.selectedIndex > 0) { + this.deselectItem(this.selectedIndex); + this.selectedIndex--; + this.selectItem(this.selectedIndex); + } + }, + + selectItem: function (index) { + if (index >= 0 && index < this.collection.length) { + var view = this.children.findByIndex(index); + if (view != null) { + view.select(); + } + } + }, + + deselectItem: function (index) { + var view = this.children.findByIndex(index); + if (view != null) { + view.deselect(); + } + } + }); + +}); diff --git a/server/sonar-web/src/main/js/nav/app.js b/server/sonar-web/src/main/js/nav/app.js new file mode 100644 index 00000000000..1f3722c3465 --- /dev/null +++ b/server/sonar-web/src/main/js/nav/app.js @@ -0,0 +1,34 @@ +define([ + 'nav/global-navbar-view', + 'nav/context-navbar-view' +], function (GlobalNavbarView, ContextNavbarView) { + + var $ = jQuery, + App = new Marionette.Application(); + + App.addInitializer(function () { + this.navbarView = new GlobalNavbarView({ + app: App, + el: $('.navbar-global'), + collection: new Backbone.Collection(window.navbarGlobalMenu) + }); + this.navbarView.render(); + }); + + if (window.navbarBreadcrumbs != null) { + App.addInitializer(function () { + this.contextNavbarView = new ContextNavbarView({ + app: App, + el: $('.navbar-context'), + collection: new Backbone.Collection(window.navbarContextMenu), + breadcrumbs: new Backbone.Collection(window.navbarBreadcrumbs), + }); + this.contextNavbarView.render(); + }); + } + + window.requestMessages().done(function () { + App.start(); + }); + +}); diff --git a/server/sonar-web/src/main/js/nav/context-navbar-view.js b/server/sonar-web/src/main/js/nav/context-navbar-view.js new file mode 100644 index 00000000000..ec6f39fedf7 --- /dev/null +++ b/server/sonar-web/src/main/js/nav/context-navbar-view.js @@ -0,0 +1,17 @@ +define([ + 'templates/nav' +], function () { + + var $ = jQuery; + + return Marionette.ItemView.extend({ + template: Templates['nav-context-navbar'], + + serializeData: function () { + return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), { + breadcrumbs: this.options.breadcrumbs.toJSON() + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/nav/global-navbar-view.js b/server/sonar-web/src/main/js/nav/global-navbar-view.js new file mode 100644 index 00000000000..e87ecc6d3f7 --- /dev/null +++ b/server/sonar-web/src/main/js/nav/global-navbar-view.js @@ -0,0 +1,83 @@ +define([ + 'nav/search-view', + 'templates/nav' +], function (SearchView) { + + var $ = jQuery; + + return Marionette.Layout.extend({ + template: Templates['nav-global-navbar'], + + regions: { + searchRegion: '.js-search-region' + }, + + events: { + 'click .js-login': 'onLoginClick', + 'click .js-favorite': 'onFavoriteClick', + 'show.bs.dropdown .js-search-dropdown': 'onSearchDropdownShow', + 'hidden.bs.dropdown .js-search-dropdown': 'onSearchDropdownHidden' + }, + + initialize: function () { + this.projectName = window.navbarProject; + this.projectKey = window.navbarProjectKey; + this.isProjectFavorite = window.navbarProjectFavorite; + }, + + onRender: function () { + var that = this; + this.$el.addClass('navbar-' + window.navbarSpace); + this.$el.addClass('navbar-fade'); + setTimeout(function () { + that.$el.addClass('in'); + }, 0); + }, + + onLoginClick: function () { + var returnTo = window.location.pathname + window.location.search; + window.location = baseUrl + '/sessions/new?return_to=' + encodeURIComponent(returnTo) + window.location.hash; + return false; + }, + + onFavoriteClick: function () { + var that = this, + p = window.process.addBackgroundProcess(), + url = baseUrl + '/favourites/toggle/' + window.navbarProjectId; + return $.post(url).done(function () { + that.isProjectFavorite = !that.isProjectFavorite; + that.render(); + window.process.finishBackgroundProcess(p); + }).fail(function () { + window.process.failBackgroundProcess(p); + }); + }, + + onSearchDropdownShow: function () { + var that = this; + this.searchRegion.show(new SearchView({ + hide: function () { + that.$('.js-search-dropdown-toggle').dropdown('toggle'); + } + })); + }, + + onSearchDropdownHidden: function () { + this.searchRegion.reset(); + }, + + serializeData: function () { + return _.extend(Marionette.Layout.prototype.serializeData.apply(this, arguments), { + user: window.SS.user, + userName: window.SS.userName, + isUserAdmin: window.SS.isUserAdmin, + + projectName: this.projectName, + projectKey: this.projectKey, + projectFavorite: this.isProjectFavorite, + navbarCanFavoriteProject: window.navbarCanFavoriteProject + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/nav/search-view.js b/server/sonar-web/src/main/js/nav/search-view.js new file mode 100644 index 00000000000..6efd0ca8f92 --- /dev/null +++ b/server/sonar-web/src/main/js/nav/search-view.js @@ -0,0 +1,154 @@ +define([ + 'common/selectable-collection-view', + 'templates/nav' +], function (SelectableCollectionView) { + + var $ = jQuery, + + SearchItemView = Marionette.ItemView.extend({ + tagName: 'li', + template: Templates['nav-search-item'], + + select: function () { + this.$el.addClass('active'); + }, + + deselect: function () { + this.$el.removeClass('active'); + }, + + submit: function () { + this.$('a')[0].click(); + } + }), + + SearchEmptyView = Marionette.ItemView.extend({ + tagName: 'li', + template: Templates['nav-search-empty'] + }), + + SearchResultsView = SelectableCollectionView.extend({ + className: 'menu', + tagName: 'ul', + itemView: SearchItemView, + emptyView: SearchEmptyView + }); + + return Marionette.Layout.extend({ + className: 'navbar-search', + tagName: 'form', + template: Templates['nav-search'], + + regions: { + resultsRegion: '.js-search-results' + }, + + events: { + 'submit': 'onSubmit', + 'keydown .js-search-input': 'onKeyDown', + 'keyup .js-search-input': 'debouncedOnKeyUp' + }, + + initialize: function () { + this.results = new Backbone.Collection(); + this.resetResultsToDefault(); + this.resultsView = new SearchResultsView({ collection: this.results }); + this.debouncedOnKeyUp = _.debounce(this.onKeyUp, 400); + this._bufferedValue = ''; + }, + + onRender: function () { + var that = this; + this.resultsRegion.show(this.resultsView); + setTimeout(function () { + that.$('.js-search-input').focus(); + }, 0); + }, + + onKeyDown: function (e) { + if (e.keyCode === 38) { + this.resultsView.selectPrev(); + return false; + } + if (e.keyCode === 40) { + this.resultsView.selectNext(); + return false; + } + if (e.keyCode === 13) { + this.resultsView.submitCurrent(); + return false; + } + if (e.keyCode === 27) { + this.options.hide(); + return false; + } + }, + + onKeyUp: function () { + var value = this.$('.js-search-input').val(); + if (value === this._bufferedValue) { + return; + } + this._bufferedValue = this.$('.js-search-input').val(); + this.search(value); + }, + + onSubmit: function () { + return false; + }, + + resetResultsToDefault: function () { + var recentHistory = JSON.parse(localStorage.getItem('sonar_recent_history')), + history = (recentHistory || []).map(function (historyItem) { + return { + url: baseUrl + '/dashboard/index?id=' + encodeURIComponent(historyItem.key) + dashboardParameters(true), + name: historyItem.name, + q: historyItem.icon + }; + }), + qualifiers = window.navbarQualifiers.map(function (q) { + return { + url: baseUrl + '/all_projects?qualifier=' + encodeURIComponent(q), + name: t('qualifiers.all', q) + }; + }); + this.results.reset(history.concat(qualifiers)); + }, + + search: function (q) { + if (q.length < 2) { + this.resetResultsToDefault(); + return; + } + var that = this, + url = baseUrl + '/api/components/suggestions', + options = { s: q }, + p = window.process.addBackgroundProcess(); + return $.get(url, options).done(function (r) { + var collection = []; + r.results.forEach(function (domain) { + domain.items.forEach(function (item, index) { + var title = item.name, + subtitle = null; + if (domain.q === 'FIL' || domain.q === 'UTS') { + subtitle = title.substr(0, title.lastIndexOf('/') - 1); + title = title.substr(title.lastIndexOf('/') + 1); + } + collection.push(_.extend(item, { + q: domain.q, + title: title, + subtitle: subtitle, + extra: index === 0 ? domain.name : ' ', + url: baseUrl + '/dashboard/index?id=' + encodeURIComponent(item.key) + dashboardParameters(true) + })); + }); + }); + that.results.reset(collection); + window.process.finishBackgroundProcess(p); + }).fail(function() { + window.process.failBackgroundProcess(p); + }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/third-party/bootstrap/dropdown.js b/server/sonar-web/src/main/js/third-party/bootstrap/dropdown.js new file mode 100644 index 00000000000..69698bcd2c5 --- /dev/null +++ b/server/sonar-web/src/main/js/third-party/bootstrap/dropdown.js @@ -0,0 +1,161 @@ +/* ======================================================================== + * Bootstrap: dropdown.js v3.3.1 + * http://getbootstrap.com/javascript/#dropdowns + * ======================================================================== + * Copyright 2011-2015 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * ======================================================================== */ + + ++function ($) { + 'use strict'; + + // DROPDOWN CLASS DEFINITION + // ========================= + + var backdrop = '.dropdown-backdrop' + var toggle = '[data-toggle="dropdown"]' + var Dropdown = function (element) { + $(element).on('click.bs.dropdown', this.toggle) + } + + Dropdown.VERSION = '3.3.1' + + Dropdown.prototype.toggle = function (e) { + var $this = $(this) + + if ($this.is('.disabled, :disabled')) return + + var $parent = getParent($this) + var isActive = $parent.hasClass('open') + + clearMenus() + + if (!isActive) { + if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { + // if mobile we use a backdrop because click events don't delegate + $('