diff options
15 files changed, 1068 insertions, 464 deletions
diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/issues/_filter_favourites2.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/issues/_filter_favourites2.html.erb index dc1bd72a906..4824fa99474 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/issues/_filter_favourites2.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/issues/_filter_favourites2.html.erb @@ -1,21 +1,7 @@ <% if logged_in? %> -<div style="border-bottom: 1px solid #ccc; padding-bottom: 10px;"> - <ul> - <% if !@favourite_filters.empty? %> - <% @favourite_filters.each do |filter| %> - <li> - <a href="<%= ApplicationController.root_context -%>/issues/filter/<%= filter.id -%>"><%= h filter.name -%></a> - </li> - <% end %> - <% else %> - <li>No favorite filters</li> + { + <% @favourite_filters.each do |filter| %> + '<%= filter.id -%>': '<%= h filter.name -%>', <% end %> - </ul> -</div> - -<div> - <ul> - <li><a href="<%= ApplicationController.root_context -%>/issues/manage" class="link-action"><%= message('manage') %></a></li> - </ul> -</div> + } <% end %> diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/issues/search2.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/issues/search2.html.erb index 5a79e409b2f..daef4f4f011 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/issues/search2.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/issues/search2.html.erb @@ -18,45 +18,92 @@ } </script> -<script id="filterTemplate" type="text/template"> +<script id="baseFilterTemplate" type="text/template"> <div class="navigator-filter-label">{{ name }}</div> - <div class="navigator-filter-body">{{ body }}</div> + <div class="navigator-filter-value {[ if (defaultValue) { ]}default{[ } ]}">{{ value }}</div> +</script> + +<script id="detailsFilterTemplate" type="text/template"> + <div class="navigator-filter-details-inner"> + No details + </div> </script> <script id="filterBarTemplate" type="text/template"> - <form action="<%= ApplicationController.root_context -%>/issues/search" method="get"> - <div class="navigator-filters-favorite"> - <a class="navigator-filters-favorite-toggle" onclick="showDropdownMenu('favorite-filters'); return false;"></a> - - <div id="favorite-filters" class="dropdown-menu" style="max-width: none; display: none; font-size: 12px;"> - <%= render :partial => 'issues/filter_favourites2' -%> - </div> - </div> - - <div class="navigator-filters-list"></div> - <div class="navigator-filters-actions"> - <select class="navigator-disabled-filters"></select> - </div> - </form> + <div class="navigator-filters-list"></div> </script> <script id="selectFilterTemplate" type="text/template"> - <select multiple="multiple" name="{{ property }}"> - {[ _.each(choices, function(choice) { ]} - <option value="{[ print(choice[1]); ]}">{[ print(choice[0]); ]}</option> + <ul class="navigator-filter-select-list"> + {[ _.each(choices, function(value, key) { ]} + <li> + <label> + <input type="checkbox" name="{{ property }}" value="{{ key }}"> + <span>{{ value }}</span> + </label> + </li> + {[ }); ]} + </ul> +</script> + +<script id="projectFilterTemplate" type="text/template"> + <div class="navigator-filter-search"> + <input type="text"> + </div> +</script> + +<script id="projectSuggestionsFilterTemplate" type="text/template"> + {[ if (suggestions.length > 0) { ]} + {[ _.each(suggestions, function(s) { ]} + <li> + <label> + <input type="checkbox" name="{{ property }}" value="{{ s.id }}"> + <span>{{ s.text }}</span> + </label> + </li> {[ }); ]} - </select> + {[ } else { ]} + <li><label>Start typing to get results</label></li> + {[ } ]} </script> -<script id="ajaxSelectFilterTemplate" type="text/template"> - <input type="hidden" name="{{ property }}"> +<script id="projectSuggestionTemplate" type="text/template"> + <label> + <input type="checkbox" value="{{ id }}" {[ if (selected) { ]}checked{[ } ]}> + <span>{{ text }}</span> + </label> +</script> + +<script id="projectNoSuggestionsTemplate" type="text/template"> + No results </script> <script id="rangeFilterTemplate" type="text/template"> - <input class="navigator-filter-range-input" type="text" - name="{{ propertyFrom }}" placeholder="From"> - <input class="navigator-filter-range-input" type="text" - name="{{ propertyTo }}" placeholder="To"> + <div class="navigator-filter-details-inner"> + <input class="navigator-filter-range-input" type="text" + name="{{ propertyFrom }}"> + <label>to</label> + <input class="navigator-filter-range-input" type="text" + name="{{ propertyTo }}"> + </div> +</script> + +<script id="favoriteFilterTemplate" type="text/template"> + <div class="navigator-filter-favorite"></div> +</script> + +<script id="detailsFavoriteFilterTemplate" type="text/template"> + <ul class="navigator-filter-select-list"> + {[ _.each(choices, function(value, key) { ]} + <li> + <label data-id="{{ key }}">{{ value }}</label> + </li> + {[ }); ]} + <li class="line"></li> + <li class="manage"> + <label>Manage</label> + </li> + </ul> </script> <script> @@ -68,7 +115,8 @@ _.extend(window.SS, { severities: <%= RulesConfigurationController::RULE_PRIORITIES.to_json.html_safe -%>, statuses: <%= @options_for_statuses.to_json.html_safe -%>, - resolutions: <%= @options_for_resolutions.to_json.html_safe -%> + resolutions: <%= @options_for_resolutions.to_json.html_safe -%>, + favorites: <%= render :partial => 'issues/filter_favourites2' -%> }); window.SS.NavigatorApp.start(); diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb index 6eb48e182db..6a2893338de 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb @@ -51,7 +51,11 @@ <%= javascript_include_tag 'select-list' %> - <%= javascript_include_tag 'navigator/filters' %> + <%= javascript_include_tag 'navigator/filters/base-filters' %> + <%= javascript_include_tag 'navigator/filters/select-filters' %> + <%= javascript_include_tag 'navigator/filters/ajax-select-filters' %> + <%= javascript_include_tag 'navigator/filters/range-filters' %> + <%= javascript_include_tag 'navigator/filters/favorite-filters' %> <%= javascript_include_tag 'navigator/app' %> <%= javascript_include_tag 'application' %> diff --git a/sonar-server/src/main/webapp/javascripts/navigator/app.js b/sonar-server/src/main/webapp/javascripts/navigator/app.js index 6a01e062631..5df314f3708 100644 --- a/sonar-server/src/main/webapp/javascripts/navigator/app.js +++ b/sonar-server/src/main/webapp/javascripts/navigator/app.js @@ -17,20 +17,17 @@ window.SS = typeof window.SS === 'object' ? window.SS : {}; NavigatorApp.addInitializer(function() { this.filters = new window.SS.Filters([ + new window.SS.Filter({ + type: window.SS.FavoriteFilterView, + enabled: true, + choices: window.SS.favorites + }), + new window.SS.Filter({ name: 'Project', property: 'componentRoots', - type: window.SS.AjaxSelectFilterView, - enabled: true, - select2 : { - allowClear: true, - ajax: { - quietMillis: 300, - url: baseUrl + '/api/resources/search?f=s2&q=TRK&display_key=true', - data: function (term, page) { return { s: term, p: page }; }, - results: function (data) { return { more: data.more, results: data.results }; } - } - } + type: window.SS.ProjectFilterView, + enabled: true }), new window.SS.Filter({ @@ -38,7 +35,13 @@ window.SS = typeof window.SS === 'object' ? window.SS : {}; property: 'severities[]', type: window.SS.SelectFilterView, enabled: true, - choices: window.SS.severities + choices: { + 'BLOCKER': 'Blocker', + 'CRITICAL': 'Critical', + 'MAJOR': 'Major', + 'MINOR': 'Minor', + 'INFO': 'Info' + } }), new window.SS.Filter({ @@ -46,52 +49,66 @@ window.SS = typeof window.SS === 'object' ? window.SS : {}; property: 'statuses[]', type: window.SS.SelectFilterView, enabled: true, - choices: window.SS.statuses + choices: { + 'OPEN': 'Open', + 'CONFIRMED': 'Confirmed', + 'REOPENED': 'Reopened', + 'RESOLVED': 'Resolved', + 'CLOSED': 'Closed' + } }), new window.SS.Filter({ name: 'Resolution', property: 'resolutions[]', type: window.SS.SelectFilterView, - enabled: false, - choices: window.SS.resolutions + enabled: true, + choices: { + 'FALSE-POSITIVE': 'False positive', + 'FIXED': 'Fixed', + 'REMOVED': 'Removed' + } }), new window.SS.Filter({ name: 'Assignee', property: 'assignees', - type: window.SS.AjaxSelectFilterView, - enabled: true, - select2: { - allowClear: true, - query: - function(query) { - if (query.term.length === 0) { - query.callback({results: [{id:'<unassigned>',text:'Unassigned'}]}); - } else if (query.term.length >= 2) { - $j.ajax(baseUrl + '/api/users/search?f=s2', { - data: {s: query.term}, - dataType: 'jsonp' - }).done(function(data) { - query.callback(data); - }); - } - } - } + type: window.SS.AssigneeFilterView, + enabled: true//, +// select2: { +// allowClear: true, +// query: +// function(query) { +// if (query.term.length === 0) { +// query.callback({results: [{id:'<unassigned>',text:'Unassigned'}]}); +// } else if (query.term.length >= 2) { +// $j.ajax(baseUrl + '/api/users/search?f=s2', { +// data: {s: query.term}, +// dataType: 'jsonp' +// }).done(function(data) { +// query.callback(data); +// }); +// } +// } +// } }), new window.SS.Filter({ name: 'Created', propertyFrom: 'createdAfter', propertyTo: 'createdBefore', - type: window.SS.RangeFilterView, - enabled: false + type: window.SS.DateRangeFilterView, + enabled: true }) ]); this.filterBarView = new window.SS.FilterBarView({ - collection: this.filters + collection: this.filters, + extra: { + sort: '', + asc: false + } }); this.filtersRegion.show(this.filterBarView); diff --git a/sonar-server/src/main/webapp/javascripts/navigator/filters.js b/sonar-server/src/main/webapp/javascripts/navigator/filters.js deleted file mode 100644 index 4d99cdf515a..00000000000 --- a/sonar-server/src/main/webapp/javascripts/navigator/filters.js +++ /dev/null @@ -1,289 +0,0 @@ -/* global _:false, $j:false, Backbone:false, baseUrl:false */ - -window.SS = typeof window.SS === 'object' ? window.SS : {}; - -(function() { - - var Filter = Backbone.Model.extend({}); - - - - var Filters = Backbone.Collection.extend({ - model: Filter - }); - - - - var BaseFilterView = Backbone.Marionette.ItemView.extend({ - template: '#filterTemplate', - className: 'navigator-filter', - - - events: function() { - return {}; - }, - - - modelEvents: { - "change:enabled": "render" - }, - - - initialize: function() { - Backbone.Marionette.ItemView.prototype.initialize.call(this, arguments); - this.model.view = this; - }, - - - render: function() { - Backbone.Marionette.ItemView.prototype.render.call(this, arguments); - - this.$el.toggleClass( - 'navigator-filter-disabled', - !this.model.get('enabled')); - }, - - - renderBody: function() { - return ''; - }, - - - serializeData: function() { - return _.extend({}, this.model.toJSON(), { - body: this.renderBody() - }); - }, - - - restore: function() {} - }); - - - - var SelectFilterView = BaseFilterView.extend({ - - renderBody: function() { - var template = _.template($j('#selectFilterTemplate').html()); - return template(this.model.toJSON()); - }, - - - onDomRefresh: function() { - var that = this; - - this.$('.navigator-filter-label').hide(); - - this.$(':input').select2({ - allowClear: false, - placeholder: this.model.get('name'), - width: '150px' - }).on('change', function(e) { - that.model.set('value', e.val); - }); - }, - - - restore: function() { - if (this.model.get('value')) { - this.$(':input').select2('val', this.model.get('value')); - } - }, - - - focus: function() {} - }); - - - - var AjaxSelectFilterView = BaseFilterView.extend({ - - renderBody: function() { - var template = _.template($j('#ajaxSelectFilterTemplate').html()); - return template(this.model.toJSON()); - }, - - onDomRefresh: function() { - var that = this; - - this.$('.navigator-filter-label').hide(); - - this.$(':input').select2(_.extend({ - allowClear: false, - placeholder: this.model.get('name'), - width: '150px', - minimumInputLength: 2 - }, this.model.get('select2'))) - .on('change', function(e) { - that.model.set('value', e.val); - }); - }, - - restore: function() { - if (this.model.get('value')) { - this.$(':input').select2('data', this.model.get('value')); - } - }, - - focus: function() {} - }); - - - - var RangeFilterView = BaseFilterView.extend({ - - events: function() { - return _.extend(BaseFilterView.prototype.events.call(), { - 'change input': 'changeInput' - }); - }, - - - renderBody: function() { - var template = _.template($j('#rangeFilterTemplate').html()); - return template(this.model.toJSON()); - }, - - - onRender: function() { - this.inputFrom = this.$('[name="' + this.model.get('propertyFrom') + '"]'); - this.inputTo = this.$('[name="' + this.model.get('propertyTo') + '"]'); - }, - - - changeInput: function() { - this.model.set('value', { - from: this.inputFrom.val(), - to: this.inputTo.val() - }); - }, - - - restore: function() { - var value = this.model.get('value'); - if (typeof value === 'object') { - this.inputFrom.val(value.from || ''); - this.inputTo.val(value.to || ''); - } - }, - - - focus: function() { - this.inputFrom.focus(); - } - - }); - - - var FilterBarView = Backbone.Marionette.CompositeView.extend({ - template: '#filterBarTemplate', - itemViewContainer: '.navigator-filters-list', - - - collectionEvents: { - 'change:value': 'changeFilters', - 'change:enabled': 'renderDisabledFilters' - }, - - - ui: { - disabledFilters: '.navigator-disabled-filters' - }, - - - getItemView: function(item) { - return item.get('type') || BaseFilterView; - }, - - - itemViewOptions: function() { - return { - filterBarView: this - }; - }, - - - render: function() { - Backbone.Marionette.CompositeView.prototype.render.call(this, arguments); - this.renderDisabledFilters(); - }, - - - renderDisabledFilters: function() { - var that = this, - disabledFilters = this.collection.where({ enabled: false }); - - that.ui.disabledFilters.select2('destroy').empty().show(); - - if (disabledFilters.length > 0) { - $j('<option>').appendTo(that.ui.disabledFilters); - _.each(disabledFilters, function(item) { - $j('<option>').text(item.get('name')).prop('value', item.cid) - .appendTo(that.ui.disabledFilters); - }); - that.ui.disabledFilters.select2({ - allowClear: true, - placeholder: 'More criteria', - width: '150px' - }).on('change', function(e) { - that.ui.disabledFilters.select2('val', ''); - that.enableFilter(e.val); - }); - } else { - that.ui.disabledFilters.hide(); - } - }, - - - enableFilter: function(key) { - var item = this.collection.find(function(item) { - return item.cid === key; - }); - - if (item) { - item.view.$el.detach().appendTo(this.itemViewContainer); - item.set('enabled', true); - item.view.focus(); - } - }, - - - changeFilters: function() { - var query = {}; - this.collection.each(function(item) { - if (item.get('value')) { - query[item.get('property')] = item.get('value'); - } - }); - this.applyQuery($j.param(query)); - }, - - - applyQuery: function(query) { - $j.ajax({ - url: baseUrl + '/issues/search', - type: 'get', - data: query - }).done(function(r) { - $j('.navigator-results').html(r); - }); - } - }); - - - - /* - * Export public classes - */ - - _.extend(window.SS, { - Filter: Filter, - Filters: Filters, - BaseFilterView: BaseFilterView, - FilterBarView: FilterBarView, - SelectFilterView: SelectFilterView, - AjaxSelectFilterView: AjaxSelectFilterView, - RangeFilterView: RangeFilterView - }); - -})(); diff --git a/sonar-server/src/main/webapp/javascripts/navigator/filters/ajax-select-filters.js b/sonar-server/src/main/webapp/javascripts/navigator/filters/ajax-select-filters.js new file mode 100644 index 00000000000..48a6298f775 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/navigator/filters/ajax-select-filters.js @@ -0,0 +1,260 @@ +/* global _:false, $j:false, Backbone:false, baseUrl:false */ + +window.SS = typeof window.SS === 'object' ? window.SS : {}; + +(function() { + + var AssigneeSuggestions = Backbone.Collection.extend({ + + parse: function(r) { + return r.results; + }, + + + url: function() { + return baseUrl + '/api/users/search?f=s2'; + } + + }); + + + + var ProjectSuggestions = Backbone.Collection.extend({ + + parse: function(r) { + return r.results; + }, + + + url: function() { + return baseUrl + '/api/resources/search?f=s2&q=TRK&display_key=true'; + } + + }); + + + + var AjaxSelectSuggestionView = Backbone.Marionette.ItemView.extend({ + template: '#projectSuggestionTemplate', + tagName: 'li', + + + events: { + 'change input[type=checkbox]': 'changeSelection' + }, + + + changeSelection: function() { + this.options.detailsView.updateSelection(); + }, + + + isModelSelected: function() { + var value = this.options.detailsView.model.get('value'); + return (_.isArray(value) && _.indexOf(value, this.model.get('id')) !== -1); + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + selected: this.isModelSelected() + }); + } + + }); + + + + var AjaxSelectNoSuggestionsView = Backbone.Marionette.ItemView.extend({ + template: '#projectNoSuggestionsTemplate', + tagName: 'li', + className: 'single' + }); + + + + var AjaxSelectSuggestionsView = Backbone.Marionette.CollectionView.extend({ + itemView: AjaxSelectSuggestionView, + emptyView: AjaxSelectNoSuggestionsView, + tagName: 'ul', + className: 'navigator-filter-select-list', + + + itemViewOptions: function() { + return { + detailsView: this.options.detailsView + }; + } + + }); + + + + var AjaxSelectDetailsFilterView = window.SS.DetailsFilterView.extend({ + template: '#projectFilterTemplate', + + + initialize: function() { + window.SS.DetailsFilterView.prototype.initialize.apply(this, arguments); + + this.suggestions = new ProjectSuggestions(); + this.suggestionsView = new AjaxSelectSuggestionsView({ + collection: this.suggestions, + model: this.model, + detailsView: this + }); + }, + + + bindSearchEvent: function() { + var that = this, + keyup = function() { that.search(); }; + + this.$('.navigator-filter-search input') + .on('keyup', $j.debounce(250, keyup)); + }, + + + onRender: function() { + this.bindSearchEvent(); + this.suggestionsView.$el.appendTo(this.$el); + this.suggestions.reset([]); + }, + + + onShow: function() { + this.$('.navigator-filter-search input').focus(); + }, + + + search: function() { + var query = this.$('.navigator-filter-search input').val(); + if (query.length > 1) { + this.suggestions.fetch({ + data: { s: query }, + reset: true + }); + } else { + this.suggestions.reset([]); + } + }, + + + updateSelection: function() { + var value = this.$('input[type=checkbox]:checked').map(function () { + return $j(this).val(); + }).get(), + + textValue = this.$('input[type=checkbox]:checked').map(function () { + return $j(this).next().text(); + }).get(); + + this.model.set({ + value: value, + textValue: textValue + }); + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + suggestions: this.suggestions.toJSON() + }); + } + + }); + + + + var AjaxSelectFilterView = window.SS.BaseFilterView.extend({ + + initialize: function() { + window.SS.BaseFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + }, + + + renderValue: function() { + var value = this.model.get('textValue'); + return this.isDefaultValue() ? 'All' : value.join(', '); + }, + + + isDefaultValue: function() { + var value = this.model.get('value'); + return !(_.isArray(value) && value.length > 0); + } + + }); + + + + var DetailsProjectFilterView = AjaxSelectDetailsFilterView.extend({ + + initialize: function() { + AjaxSelectDetailsFilterView.prototype.initialize.apply(this, arguments); + + this.suggestions = new ProjectSuggestions(); + this.suggestionsView = new AjaxSelectSuggestionsView({ + collection: this.suggestions, + model: this.model, + detailsView: this + }); + } + + }); + + + + var ProjectFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + window.SS.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsProjectFilterView + }); + } + + }); + + + + var DetailsAssigneeFilterView = AjaxSelectDetailsFilterView.extend({ + + initialize: function() { + AjaxSelectDetailsFilterView.prototype.initialize.apply(this, arguments); + + this.suggestions = new AssigneeSuggestions(); + this.suggestionsView = new AjaxSelectSuggestionsView({ + collection: this.suggestions, + model: this.model, + detailsView: this + }); + } + + }); + + + + var AssigneeFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + window.SS.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsAssigneeFilterView + }); + } + + }); + + + + /* + * Export public classes + */ + + _.extend(window.SS, { + ProjectFilterView: ProjectFilterView, + AssigneeFilterView: AssigneeFilterView + }); + +})(); diff --git a/sonar-server/src/main/webapp/javascripts/navigator/filters/base-filters.js b/sonar-server/src/main/webapp/javascripts/navigator/filters/base-filters.js new file mode 100644 index 00000000000..15b302ea016 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/navigator/filters/base-filters.js @@ -0,0 +1,228 @@ +/* global _:false, $j:false, Backbone:false, baseUrl:false */ + +window.SS = typeof window.SS === 'object' ? window.SS : {}; + +(function() { + + var Filter = Backbone.Model.extend({}); + + + + var Filters = Backbone.Collection.extend({ + model: Filter + }); + + + + var DetailsFilterView = Backbone.Marionette.ItemView.extend({ + template: '#detailsFilterTemplate', + className: 'navigator-filter-details', + + onShow: function() {}, + onHide: function() {} + }); + + + + var BaseFilterView = Backbone.Marionette.ItemView.extend({ + template: '#baseFilterTemplate', + className: 'navigator-filter', + + + events: function() { + return { + 'click': 'toggleDetails' + }; + }, + + + initialize: function(options) { + Backbone.Marionette.ItemView.prototype.initialize.apply(this, arguments); + + var detailsView = options.detailsView || DetailsFilterView; + this.detailsView = new detailsView({ + model: this.model + }); + this.detailsView.render = this.renderDetails; + + this.model.view = this; + + this.listenTo(this.model, 'change:value', this.renderBase); + }, + + + attachDetailsView: function() { + this.detailsView.$el.detach().appendTo($j('body')); + }, + + + render: function() { + this.renderBase(); + + this.attachDetailsView(); + this.detailsView.render.call(this.detailsView); + + this.$el.toggleClass( + 'navigator-filter-disabled', + !this.model.get('enabled')); + }, + + + renderBase: function() { + Backbone.Marionette.ItemView.prototype.render.apply(this, arguments); + }, + + + toggleDetails: function(e) { + e.stopPropagation(); + if (this.$el.hasClass('active')) { + this.hideDetails(); + } else { + this.showDetails(); + } + }, + + + showDetails: function() { + this.registerShowedDetails(); + + var top = this.$el.offset().top + this.$el.outerHeight(), + left = this.$el.offset().left; + + this.detailsView.$el.css({ top: top, left: left }).addClass('active'); + this.$el.addClass('active'); + this.detailsView.onShow(); + }, + + + registerShowedDetails: function() { + this.options.filterBarView.hideDetails(); + this.options.filterBarView.showedView = this; + }, + + + hideDetails: function() { + this.detailsView.$el.removeClass('active'); + this.$el.removeClass('active'); + this.detailsView.onHide(); + }, + + + renderValue: function() { + return this.model.get('value') || 'unset'; + }, + + + renderDetails: function() { + Backbone.Marionette.ItemView.prototype.render.apply(this, arguments); + }, + + + isDefaultValue: function() { + return true; + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + value: this.renderValue(), + defaultValue: this.isDefaultValue() + }); + } + + }); + + + + var FilterBarView = Backbone.Marionette.CompositeView.extend({ + template: '#filterBarTemplate', + itemViewContainer: '.navigator-filters-list', + + + collectionEvents: { + 'change:value': 'changeFilters' + }, + + + ui: { + disabledFilters: '.navigator-disabled-filters' + }, + + + getItemView: function(item) { + return item.get('type') || BaseFilterView; + }, + + + itemViewOptions: function() { + return { + filterBarView: this + }; + }, + + + initialize: function() { + Backbone.Marionette.CompositeView.prototype.initialize.apply(this, arguments); + var that = this; + $('body').on('click', function() { + that.hideDetails(); + }); + }, + + + render: function() { + Backbone.Marionette.CompositeView.prototype.render.apply(this, arguments); + }, + + + hideDetails: function() { + if (_.isObject(this.showedView)) { + this.showedView.hideDetails(); + } + }, + + + changeFilters: function() { + var query = _.clone(this.options.extra); + this.collection.each(function(item) { + var value = item.get('value'); + + if (value) { + if (_.isObject(value) && !_.isArray(value)) { + _.extend(query, value); + } else { + query[item.get('property')] = item.get('value'); + } + } + + }); + this.applyQuery($j.param(query)); + }, + + + applyQuery: function(query) { + $j.ajax({ + url: baseUrl + '/issues/search', + type: 'get', + data: query + }).done(function(r) { + $j('.navigator-results').html(r); + }); + } + }); + + + + /* + * Export public classes + */ + + _.extend(window.SS, { + Filter: Filter, + Filters: Filters, + BaseFilterView: BaseFilterView, + DetailsFilterView: DetailsFilterView, + FilterBarView: FilterBarView + }); + +})(); diff --git a/sonar-server/src/main/webapp/javascripts/navigator/filters/favorite-filters.js b/sonar-server/src/main/webapp/javascripts/navigator/filters/favorite-filters.js new file mode 100644 index 00000000000..52971fe3002 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/navigator/filters/favorite-filters.js @@ -0,0 +1,63 @@ +/* global _:false, $j:false, baseUrl:false */ + +window.SS = typeof window.SS === 'object' ? window.SS : {}; + +(function() { + + var DetailsFavoriteFilterView = window.SS.DetailsFilterView.extend({ + template: '#detailsFavoriteFilterTemplate', + + + events: { + 'click label[data-id]': 'applyFavorite', + 'click .manage label': 'manage' + }, + + + applyFavorite: function(e) { + var id = $j(e.target).data('id'); + window.location = baseUrl + '/issues/filter/' + id; + }, + + + manage: function() { + window.location = baseUrl + '/issues/manage'; + } + + }); + + + + var FavoriteFilterView = window.SS.SelectFilterView.extend({ + template: '#favoriteFilterTemplate', + + + initialize: function() { + window.SS.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsFavoriteFilterView + }); + }, + + + renderValue: function() { + return ''; + }, + + + isDefaultValue: function() { + return false; + } + + }); + + + + /* + * Export public classes + */ + + _.extend(window.SS, { + FavoriteFilterView: FavoriteFilterView + }); + +})(); diff --git a/sonar-server/src/main/webapp/javascripts/navigator/filters/range-filters.js b/sonar-server/src/main/webapp/javascripts/navigator/filters/range-filters.js new file mode 100644 index 00000000000..088cdd4165a --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/navigator/filters/range-filters.js @@ -0,0 +1,99 @@ +/* global _:false, $j:false, Backbone:false, baseUrl:false */ + +window.SS = typeof window.SS === 'object' ? window.SS : {}; + +(function() { + + var DetailsRangeFilterView = window.SS.DetailsFilterView.extend({ + template: '#rangeFilterTemplate', + + + events: { + 'change input': 'change' + }, + + + change: function() { + var value = {}, + valueFrom = this.$('input').eq(0).val(), + valueTo = this.$('input').eq(1).val(); + + if (valueFrom.length > 0) { + value[this.model.get('propertyFrom')] = valueFrom; + } + + if (valueTo.length > 0) { + value[this.model.get('propertyTo')] = valueTo; + } + + this.model.set('value', value); + } + + }); + + + + var RangeFilterView = window.SS.BaseFilterView.extend({ + + initialize: function() { + window.SS.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsRangeFilterView + }); + }, + + + renderValue: function() { + var value = this.model.get('value'); + if (_.isObject(value) && (value.from || value.to)) { + return 'From ' + (value.from || '') + ' to ' + (value.to || ''); + } else { + return 'Any'; + } + } + + }); + + + + var DateRangeFilterView = RangeFilterView.extend({ + + render: function() { + RangeFilterView.prototype.render.apply(this, arguments); + this.detailsView.$('input').prop('placeholder', '1970-01-01'); + }, + + + renderValue: function() { + if (!this.isDefaultValue()) { + var value = _.values(this.model.get('value')); + return value.join(' — '); + } else { + return 'Anytime'; + } + }, + + + isDefaultValue: function() { + var value = this.model.get('value'), + propertyFrom = this.model.get('propertyFrom'), + propertyTo = this.model.get('propertyTo'), + valueFrom = _.isObject(value) && value[propertyFrom], + valueTo = _.isObject(value) && value[propertyTo]; + + return !valueFrom && !valueTo; + } + + }); + + + + /* + * Export public classes + */ + + _.extend(window.SS, { + RangeFilterView: RangeFilterView, + DateRangeFilterView: DateRangeFilterView + }); + +})(); diff --git a/sonar-server/src/main/webapp/javascripts/navigator/filters/select-filters.js b/sonar-server/src/main/webapp/javascripts/navigator/filters/select-filters.js new file mode 100644 index 00000000000..d10391a7351 --- /dev/null +++ b/sonar-server/src/main/webapp/javascripts/navigator/filters/select-filters.js @@ -0,0 +1,66 @@ +/* global _:false, $j:false, Backbone:false, baseUrl:false */ + +window.SS = typeof window.SS === 'object' ? window.SS : {}; + +(function() { + + var DetailsSelectFilterView = window.SS.DetailsFilterView.extend({ + template: '#selectFilterTemplate', + + + events: { + 'change input[type=checkbox]': 'changeSelection' + }, + + + changeSelection: function() { + var value = this.$('input[type=checkbox]:checked').map(function() { + return $j(this).val(); + }).get(); + this.model.set('value', value); + } + }); + + + + var SelectFilterView = window.SS.BaseFilterView.extend({ + + initialize: function() { + window.SS.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsSelectFilterView + }); + }, + + + renderValue: function() { + var choices = this.model.get('choices'), + value = (this.model.get('value') || []).map(function(key) { + return choices[key]; + }); + + return this.isDefaultValue() ? 'All' : value.join(', '); + }, + + + isDefaultValue: function() { + var value = this.model.get('value'), + choices = this.model.get('choices'); + + return !(_.isArray(value) && + value.length > 0 && + value.length < Object.keys(choices).length); + } + + }); + + + + /* + * Export public classes + */ + + _.extend(window.SS, { + SelectFilterView: SelectFilterView + }); + +})(); diff --git a/sonar-server/src/main/webapp/stylesheets/navigator.css b/sonar-server/src/main/webapp/stylesheets/navigator.css index 146ed8be9bf..915976b925b 100644 --- a/sonar-server/src/main/webapp/stylesheets/navigator.css +++ b/sonar-server/src/main/webapp/stylesheets/navigator.css @@ -1,45 +1,15 @@ /* + * Fonts + */ +/* * Colors */ -.navigator .select2-container .select2-choice, -.navigator .select2-container-multi .select2-choices { - border: 1px solid #aaa; - border-radius: 0; - background: #ffffff; -} -.navigator .select2-container-active .select2-choice, -.navigator .select2-container-active .select2-choices { - box-shadow: none; -} -.navigator .select2-container-multi .select2-choices { - white-space: nowrap; -} -.navigator .select2-container-multi .select2-choices li { - float: none; - display: inline-block; - vertical-align: middle; -} .navigator-filters { margin-bottom: 10px; - padding: 10px; - border: 1px solid #cdcdcd; + border-bottom: 1px solid #cdcdcd; background: #efefef; font-size: 0; } -.navigator-filters-favorite { - display: inline-block; - vertical-align: middle; - margin-right: 15px; - padding-right: 14px; - border-right: 1px solid #cdcdcd; -} -.navigator-filters-favorite-toggle { - display: block; - width: 16px; - height: 16px; - background: url(../images/star.png) no-repeat left center; - cursor: pointer; -} .navigator-filters-list { display: inline-block; vertical-align: middle; @@ -49,14 +19,27 @@ display: inline-block; vertical-align: middle; margin-left: 20px; - font-size: 14px; + font-size: 13px; } .navigator-filter { display: inline-block; vertical-align: top; + height: 36px; + line-height: 36px; + padding: 0 7px; + white-space: nowrap; + cursor: pointer; + transition: background 0.3s ease; } -.navigator-filter + .navigator-filter { - margin-left: 20px; +.navigator-filter:hover { + background-color: #e2e2e2; +} +.navigator-filter.active { + position: relative; + padding: 0 6px; + border-left: 1px solid #cdcdcd; + border-right: 1px solid #cdcdcd; + background: #fff; } .navigator-filter-disabled { display: none; @@ -65,18 +48,85 @@ display: inline-block; vertical-align: middle; margin-right: 5px; - color: #666; - font-size: 14px; + color: #333; + font-size: 13px; } .navigator-filter-label:after { content: ":"; } -.navigator-filter-body { +.navigator-filter-value { display: inline-block; vertical-align: middle; - color: #000; - font-size: 14px; + max-width: 120px; + color: #333; + font-size: 13px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.navigator-filter-value.default { + color: #666; } .navigator-filter-range-input { - width: 80px; + width: 120px; +} +.navigator-filter-details { + display: none; + position: absolute; + z-index: 1200; + min-width: 100px; + border: 1px solid #cdcdcd; + background: #fff; + font-size: 13px; + transition: opacity 0.3s ease; +} +.navigator-filter-details.active { + display: block; +} +.navigator-filter-details-inner { + padding: 5px 7px; +} +.navigator-filter-select-list { + min-width: 150px; + max-height: 182px; + padding: 5px 0; + overflow-y: auto; +} +.navigator-filter-select-list label { + display: block; + padding: 5px 7px; + transition: background 0.3s ease; + cursor: pointer; +} +.navigator-filter-select-list label:hover { + background-color: #ededed; +} +.navigator-filter-select-list input[type=checkbox] { + position: relative; + top: -1px; + cursor: pointer; +} +.navigator-filter-select-list .single { + padding: 5px 7px; +} +.navigator-filter-select-list .line { + height: 1px; + margin: 5px 0; + background: #cdcdcd; +} +.navigator-filter-search { + margin: 7px; +} +.navigator-filter-search input { + width: 100%; + height: 26px; + padding: 0 7px; + border: 1px solid #cdcdcd; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.navigator-filter-favorite { + width: 16px; + height: 36px; + background: url('../images/star.png') no-repeat center center; } diff --git a/sonar-server/src/main/webapp/stylesheets/navigator.less b/sonar-server/src/main/webapp/stylesheets/navigator.less index 031988f5f61..789d0f61e1c 100644 --- a/sonar-server/src/main/webapp/stylesheets/navigator.less +++ b/sonar-server/src/main/webapp/stylesheets/navigator.less @@ -1,55 +1,21 @@ @import "mixins"; @import "variables"; -.navigator { - - .select2-container .select2-choice, - .select2-container-multi .select2-choices { - border: 1px solid #aaa; - border-radius: 0; - background: @white; - } +@navigatorHeight: 36px; +@navigatorHover: darken(@grey, 5%); +@navigatorFilterPadding: 7px; - .select2-container-active .select2-choice, - .select2-container-active .select2-choices { - box-shadow: none; - } - - .select2-container-multi .select2-choices { - white-space: nowrap; - } - - .select2-container-multi .select2-choices li { - float: none; - display: inline-block; - vertical-align: middle; - } +.navigator { } .navigator-filters { margin-bottom: 10px; - padding: 10px; - border: 1px solid @darkGrey; + border-bottom: 1px solid @darkGrey; background: @grey; font-size: 0; } -.navigator-filters-favorite { - display: inline-block; - vertical-align: middle; - margin-right: 15px; - padding-right: 14px; - border-right: 1px solid #cdcdcd; -} - -.navigator-filters-favorite-toggle { - display: block; - .size(16px, 16px); - background: url(../images/star.png) no-repeat left center; - cursor: pointer; -} - .navigator-filters-list { display: inline-block; vertical-align: middle; @@ -60,16 +26,30 @@ display: inline-block; vertical-align: middle; margin-left: 20px; - font-size: 14px; + font-size: @baseFontSize; } .navigator-filter { display: inline-block; vertical-align: top; -} + height: @navigatorHeight; + line-height: @navigatorHeight; + padding: 0 @navigatorFilterPadding; + white-space: nowrap; + cursor: pointer; + transition: background 0.3s ease; -.navigator-filter + .navigator-filter { - margin-left: 20px; + &:hover { + background-color: @navigatorHover; + } + + &.active { + position: relative; + padding: 0 @navigatorFilterPadding - 1px; + border-left: 1px solid #cdcdcd; + border-right: 1px solid #cdcdcd; + background: #fff; + } } .navigator-filter-disabled { @@ -80,21 +60,98 @@ display: inline-block; vertical-align: middle; margin-right: 5px; - color: #666; - font-size: 14px; + color: #333; + font-size: @baseFontSize; &:after { content: ":"; } } -.navigator-filter-body { +.navigator-filter-value { display: inline-block; vertical-align: middle; - color: #000; - font-size: 14px; + max-width: 120px; + color: #333; + font-size: @baseFontSize; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &.default { + color: #666; + } } .navigator-filter-range-input { - width: 80px; + width: 120px; +} + +.navigator-filter-details { + display: none; + position: absolute; + z-index: 1200; + min-width: 100px; + border: 1px solid #cdcdcd; + background: #fff; + font-size: @baseFontSize; + transition: opacity 0.3s ease; + + &.active { + display: block; + } +} + +.navigator-filter-details-inner { + padding: 5px @navigatorFilterPadding; +} + +.navigator-filter-select-list { + min-width: 150px; + max-height: 182px; + padding: 5px 0; + overflow-y: auto; + + label { + display: block; + padding: 5px @navigatorFilterPadding; + transition: background 0.3s ease; + cursor: pointer; + + &:hover { + background-color: darken(#fff, 7%); + } + } + + input[type=checkbox] { + position: relative; + top: -1px; + cursor: pointer; + } + + .single { + padding: 5px @navigatorFilterPadding; + } + + .line { + height: 1px; + margin: 5px 0; + background: #cdcdcd; + } +} + +.navigator-filter-search { + margin: @navigatorFilterPadding; + + input { + .size(100%, 26px); + padding: 0 7px; + border: 1px solid #cdcdcd; + .box-sizing(border-box); + } +} + +.navigator-filter-favorite { + .size(16px, @navigatorHeight); + background: url('../images/star.png') no-repeat center center; } .navigator-results { diff --git a/sonar-server/src/main/webapp/stylesheets/variables.css b/sonar-server/src/main/webapp/stylesheets/variables.css index cf549c1e46d..6fbfac8e716 100644 --- a/sonar-server/src/main/webapp/stylesheets/variables.css +++ b/sonar-server/src/main/webapp/stylesheets/variables.css @@ -1,3 +1,6 @@ /* + * Fonts + */ +/* * Colors */ diff --git a/sonar-server/src/main/webapp/stylesheets/variables.less b/sonar-server/src/main/webapp/stylesheets/variables.less index 80028758a9c..354f666ed0a 100644 --- a/sonar-server/src/main/webapp/stylesheets/variables.less +++ b/sonar-server/src/main/webapp/stylesheets/variables.less @@ -1,4 +1,12 @@ /* + * Fonts + */ + +@baseFontSize: 13px; + + + +/* * Colors */ diff --git a/sonar-server/wro.xml b/sonar-server/wro.xml index a5272ad6db0..58be3d5dbc8 100644 --- a/sonar-server/wro.xml +++ b/sonar-server/wro.xml @@ -31,7 +31,11 @@ <js>/javascripts/select-list.js</js> - <js>/javascripts/navigator/filters.js</js> + <js>/javascripts/navigator/filters/base-filters.js</js> + <js>/javascripts/navigator/filters/select-filters.js</js> + <js>/javascripts/navigator/filters/ajax-select-filters.js</js> + <js>/javascripts/navigator/filters/range-filters.js</js> + <js>/javascripts/navigator/filters/favorite-filters.js</js> <js>/javascripts/navigator/app.js</js> <js>/javascripts/application.js</js> |