aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/issues/app-context.js105
-rw-r--r--server/sonar-web/src/main/js/apps/issues/app-new.js82
-rw-r--r--server/sonar-web/src/main/js/apps/issues/component-viewer/issue-view.js18
-rw-r--r--server/sonar-web/src/main/js/apps/issues/component-viewer/main.js204
-rw-r--r--server/sonar-web/src/main/js/apps/issues/controller.js249
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets-view.js65
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/action-plan-facet.js68
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js108
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/author-facet.js44
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/base-facet.js19
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/context-facet.js16
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js131
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js70
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/file-facet.js43
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js24
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/language-facet.js67
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/module-facet.js30
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/project-facet.js83
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js46
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js52
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js78
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/status-facet.js17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js56
-rw-r--r--server/sonar-web/src/main/js/apps/issues/filters-view.js93
-rw-r--r--server/sonar-web/src/main/js/apps/issues/issue-filter-view.js29
-rw-r--r--server/sonar-web/src/main/js/apps/issues/layout.js55
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/facet.js20
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/facets.js9
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/filter.js17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/filters.js9
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/issues.js65
-rw-r--r--server/sonar-web/src/main/js/apps/issues/models/state.js58
-rw-r--r--server/sonar-web/src/main/js/apps/issues/router.js45
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/_issues-filter-name.hbs18
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-action-plan-facet.hbs18
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs30
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs10
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs3
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs32
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs14
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs10
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs20
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-filters.hbs55
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs71
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs37
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-home.hbs77
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs13
-rw-r--r--server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs5
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-header-view.js47
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-home-view.js160
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js11
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js113
-rw-r--r--server/sonar-web/src/main/js/apps/issues/workspace-list-view.js106
60 files changed, 2991 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/app-context.js b/server/sonar-web/src/main/js/apps/issues/app-context.js
new file mode 100644
index 00000000000..bcef0841779
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/app-context.js
@@ -0,0 +1,105 @@
+define([
+ './models/state',
+ './layout',
+ './models/issues',
+ 'components/navigator/models/facets',
+ './models/filters',
+ './controller',
+ './router',
+ './workspace-list-view',
+ './workspace-header-view',
+ './facets-view'
+], function (State, Layout, Issues, Facets, Filters, Controller, Router, WorkspaceListView, WorkspaceHeaderView,
+ FacetsView) {
+
+ var $ = jQuery,
+ App = new Marionette.Application();
+
+ App.getContextQuery = function () {
+ return { componentUuids: window.config.resource };
+ };
+
+ App.getRestrictedFacets = function () {
+ return {
+ 'TRK': ['projectUuids'],
+ 'BRC': ['projectUuids'],
+ 'DIR': ['projectUuids', 'moduleUuids', 'directories'],
+ 'DEV': ['authors'],
+ 'DEV_PRJ': ['projectUuids', 'authors']
+ };
+ };
+
+ App.updateContextFacets = function () {
+ var facets = this.state.get('facets'),
+ allFacets = this.state.get('allFacets'),
+ facetsFromServer = this.state.get('facetsFromServer');
+ return this.state.set({
+ facets: facets,
+ allFacets: _.difference(allFacets, this.getRestrictedFacets()[window.config.resourceQualifier]),
+ facetsFromServer: _.difference(facetsFromServer, this.getRestrictedFacets()[window.config.resourceQualifier])
+ });
+ };
+
+ App.addInitializer(function () {
+ this.state = new State({
+ isContext: true,
+ contextQuery: this.getContextQuery(),
+ contextComponentUuid: window.config.resource,
+ contextComponentName: window.config.resourceName,
+ contextComponentQualifier: window.config.resourceQualifier
+ });
+ this.updateContextFacets();
+ this.list = new Issues();
+ this.facets = new Facets();
+ this.filters = new Filters();
+ });
+
+ App.addInitializer(function () {
+ this.layout = new Layout({ app: this });
+ $('.issues').empty().append(this.layout.render().el);
+ $('#footer').addClass('search-navigator-footer');
+ });
+
+ App.addInitializer(function () {
+ this.controller = new Controller({ app: this });
+ });
+
+ App.addInitializer(function () {
+ this.issuesView = new WorkspaceListView({
+ app: this,
+ collection: this.list
+ });
+ this.layout.workspaceListRegion.show(this.issuesView);
+ this.issuesView.bindScrollEvents();
+ });
+
+ App.addInitializer(function () {
+ this.workspaceHeaderView = new WorkspaceHeaderView({
+ app: this,
+ collection: this.list
+ });
+ this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);
+ });
+
+ App.addInitializer(function () {
+ this.facetsView = new FacetsView({
+ app: this,
+ collection: this.facets
+ });
+ this.layout.facetsRegion.show(this.facetsView);
+ });
+
+ App.addInitializer(function () {
+ return this.controller.fetchFilters().done(function () {
+ key.setScope('list');
+ App.router = new Router({ app: App });
+ Backbone.history.start();
+ });
+ });
+
+ var l10nXHR = window.requestMessages();
+ return jQuery.when(l10nXHR).done(function () {
+ return App.start();
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/app-new.js b/server/sonar-web/src/main/js/apps/issues/app-new.js
new file mode 100644
index 00000000000..07b047bfcd8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/app-new.js
@@ -0,0 +1,82 @@
+define([
+ './models/state',
+ './layout',
+ './models/issues',
+ 'components/navigator/models/facets',
+ './models/filters',
+ './controller',
+ './router',
+ './workspace-list-view',
+ './workspace-header-view',
+ './facets-view',
+ './filters-view'
+], function (State, Layout, Issues, Facets, Filters, Controller, Router, WorkspaceListView, WorkspaceHeaderView,
+ FacetsView, FiltersView) {
+
+ var $ = jQuery,
+ App = new Marionette.Application();
+
+ App.addInitializer(function () {
+ this.state = new State();
+ this.list = new Issues();
+ this.facets = new Facets();
+ this.filters = new Filters();
+ });
+
+ App.addInitializer(function () {
+ this.layout = new Layout({ app: this });
+ $('.issues').empty().append(this.layout.render().el);
+ $('#footer').addClass('search-navigator-footer');
+ });
+
+ App.addInitializer(function () {
+ this.controller = new Controller({ app: this });
+ });
+
+ App.addInitializer(function () {
+ this.issuesView = new WorkspaceListView({
+ app: this,
+ collection: this.list
+ });
+ this.layout.workspaceListRegion.show(this.issuesView);
+ this.issuesView.bindScrollEvents();
+ });
+
+ App.addInitializer(function () {
+ this.workspaceHeaderView = new WorkspaceHeaderView({
+ app: this,
+ collection: this.list
+ });
+ this.layout.workspaceHeaderRegion.show(this.workspaceHeaderView);
+ });
+
+ App.addInitializer(function () {
+ this.facetsView = new FacetsView({
+ app: this,
+ collection: this.facets
+ });
+ this.layout.facetsRegion.show(this.facetsView);
+ });
+
+ App.addInitializer(function () {
+ this.filtersView = new FiltersView({
+ app: this,
+ collection: this.filters
+ });
+ this.layout.filtersRegion.show(this.filtersView);
+ });
+
+ App.addInitializer(function () {
+ this.controller.fetchFilters().done(function () {
+ key.setScope('list');
+ App.router = new Router({ app: App });
+ Backbone.history.start();
+ });
+ });
+
+ var l10nXHR = window.requestMessages();
+ return jQuery.when(l10nXHR).done(function () {
+ return App.start();
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/issue-view.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/issue-view.js
new file mode 100644
index 00000000000..6d8a6f73207
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/component-viewer/issue-view.js
@@ -0,0 +1,18 @@
+define([
+ '../workspace-list-item-view'
+], function (IssueView) {
+
+ return IssueView.extend({
+ onRender: function () {
+ IssueView.prototype.onRender.apply(this, arguments);
+ this.$el.removeClass('issue-navigate-right');
+ },
+
+ serializeData: function () {
+ return _.extend(IssueView.prototype.serializeData.apply(this, arguments), {
+ showComponent: false
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
new file mode 100644
index 00000000000..a702c4a4d7f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/component-viewer/main.js
@@ -0,0 +1,204 @@
+define([
+ 'components/source-viewer/main',
+ '../models/issues',
+ './issue-view',
+ '../templates'
+], function (SourceViewer, Issues, IssueView) {
+
+ var $ = jQuery;
+
+ return SourceViewer.extend({
+ events: function () {
+ return _.extend(SourceViewer.prototype.events.apply(this, arguments), {
+ 'click .js-close-component-viewer': 'closeComponentViewer',
+ 'click .code-issue': 'selectIssue'
+ });
+ },
+
+ initialize: function (options) {
+ SourceViewer.prototype.initialize.apply(this, arguments);
+ return this.listenTo(options.app.state, 'change:selectedIndex', this.select);
+ },
+
+ onLoaded: function () {
+ SourceViewer.prototype.onLoaded.apply(this, arguments);
+ this.bindShortcuts();
+ if (this.baseIssue != null) {
+ return this.scrollToLine(this.baseIssue.get('line'));
+ }
+ },
+
+ bindShortcuts: function () {
+ var that = this;
+ var doAction = function (action) {
+ var selectedIssueView = that.getSelectedIssueEl();
+ if (!selectedIssueView) {
+ return;
+ }
+ return selectedIssueView.find('.js-issue-' + action).click();
+ };
+ key('up', 'componentViewer', function () {
+ that.options.app.controller.selectPrev();
+ return false;
+ });
+ key('down', 'componentViewer', function () {
+ that.options.app.controller.selectNext();
+ return false;
+ });
+ key('left,backspace', 'componentViewer', function () {
+ that.options.app.controller.closeComponentViewer();
+ return false;
+ });
+ key('f', 'componentViewer', function () {
+ return doAction('transition');
+ });
+ key('a', 'componentViewer', function () {
+ return doAction('assign');
+ });
+ key('m', 'componentViewer', function () {
+ return doAction('assign-to-me');
+ });
+ key('p', 'componentViewer', function () {
+ return doAction('plan');
+ });
+ key('i', 'componentViewer', function () {
+ return doAction('set-severity');
+ });
+ return key('c', 'componentViewer', function () {
+ return doAction('comment');
+ });
+ },
+
+ unbindShortcuts: function () {
+ return key.deleteScope('componentViewer');
+ },
+
+ onClose: function () {
+ SourceViewer.prototype.onClose.apply(this, arguments);
+ this.unbindScrollEvents();
+ return this.unbindShortcuts();
+ },
+
+ select: function () {
+ var selected = this.options.app.state.get('selectedIndex'),
+ selectedIssue = this.options.app.list.at(selected);
+ if (selectedIssue.get('component') === this.model.get('key')) {
+ return this.scrollToIssue(selectedIssue.get('key'));
+ } else {
+ this.unbindShortcuts();
+ return this.options.app.controller.showComponentViewer(selectedIssue);
+ }
+ },
+
+ getSelectedIssueEl: function () {
+ var selected = this.options.app.state.get('selectedIndex');
+ if (selected == null) {
+ return null;
+ }
+ var selectedIssue = this.options.app.list.at(selected);
+ if (selectedIssue == null) {
+ return null;
+ }
+ var selectedIssueView = this.$('#issue-' + (selectedIssue.get('key')));
+ if (selectedIssueView.length > 0) {
+ return selectedIssueView;
+ } else {
+ return null;
+ }
+ },
+
+ selectIssue: function (e) {
+ var key = $(e.currentTarget).data('issue-key'),
+ issue = this.issues.find(function (issue) {
+ return issue.get('key') === key;
+ }),
+ index = this.options.app.list.indexOf(issue);
+ return this.options.app.state.set({ selectedIndex: index });
+ },
+
+ scrollToIssue: function (key) {
+ var el = this.$('#issue-' + key);
+ if (el.length > 0) {
+ var line = el.closest('[data-line-number]').data('line-number');
+ return this.scrollToLine(line);
+ } else {
+ this.unbindShortcuts();
+ var selected = this.options.app.state.get('selectedIndex'),
+ selectedIssue = this.options.app.list.at(selected);
+ return this.options.app.controller.showComponentViewer(selectedIssue);
+ }
+ },
+
+ openFileByIssue: function (issue) {
+ this.baseIssue = issue;
+ var componentKey = issue.get('component'),
+ componentUuid = issue.get('componentUuid');
+ return this.open(componentUuid, componentKey);
+ },
+
+ linesLimit: function () {
+ var line = this.LINES_LIMIT / 2;
+ if ((this.baseIssue != null) && this.baseIssue.has('line')) {
+ line = Math.max(line, this.baseIssue.get('line'));
+ }
+ return {
+ from: line - this.LINES_LIMIT / 2 + 1,
+ to: line + this.LINES_LIMIT / 2
+ };
+ },
+
+ limitIssues: function (issues) {
+ var that = this;
+ var index = this.ISSUES_LIMIT / 2;
+ if ((this.baseIssue != null) && this.baseIssue.has('index')) {
+ index = Math.max(index, this.baseIssue.get('index'));
+ }
+ return issues.filter(function (issue) {
+ return Math.abs(issue.get('index') - index) <= that.ISSUES_LIMIT / 2;
+ });
+ },
+
+ requestIssues: function () {
+ var that = this;
+ var r;
+ if (this.options.app.list.last().get('component') === this.model.get('key')) {
+ r = this.options.app.controller.fetchNextPage();
+ } else {
+ r = $.Deferred().resolve().promise();
+ }
+ return r.done(function () {
+ that.issues.reset(that.options.app.list.filter(function (issue) {
+ return issue.get('component') === that.model.key();
+ }));
+ that.issues.reset(that.limitIssues(that.issues));
+ return that.addIssuesPerLineMeta(that.issues);
+ });
+ },
+
+ renderIssues: function () {
+ this.issues.forEach(this.renderIssue, this);
+ return this.$('.source-line-issues').addClass('hidden');
+ },
+
+ renderIssue: function (issue) {
+ var issueView = new IssueView({
+ el: '#issue-' + issue.get('key'),
+ model: issue,
+ app: this.options.app
+ });
+ this.issueViews.push(issueView);
+ return issueView.render();
+ },
+
+ scrollToLine: function (line) {
+ var row = this.$('[data-line-number=' + line + ']'),
+ goal = row.length > 0 ? row.offset().top - 200 : 0;
+ return $(window).scrollTop(goal);
+ },
+
+ closeComponentViewer: function () {
+ return this.options.app.controller.closeComponentViewer();
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/controller.js b/server/sonar-web/src/main/js/apps/issues/controller.js
new file mode 100644
index 00000000000..15ccdda827f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/controller.js
@@ -0,0 +1,249 @@
+define([
+ 'components/navigator/controller',
+ './component-viewer/main',
+ './workspace-home-view'
+], function (Controller, ComponentViewer, HomeView) {
+
+ var $ = jQuery,
+ EXTRA_FIELDS = 'actions,transitions,assigneeName,reporterName,actionPlanName',
+ FACET_DATA_FIELDS = ['components', 'projects', 'users', 'rules', 'actionPlans', 'languages'];
+
+ return Controller.extend({
+ _facetsFromServer: function () {
+ var facets = Controller.prototype._facetsFromServer.apply(this, arguments) || [];
+ facets.push('assigned_to_me');
+ return facets;
+ },
+
+ _issuesParameters: function () {
+ return {
+ p: this.options.app.state.get('page'),
+ ps: this.pageSize,
+ s: 'FILE_LINE',
+ asc: true,
+ extra_fields: EXTRA_FIELDS,
+ facets: this._facetsFromServer().join()
+ };
+ },
+
+ _myIssuesFromResponse: function (r) {
+ var myIssuesData = _.findWhere(r.facets, { property: 'assigned_to_me' });
+ if ((myIssuesData != null) && _.isArray(myIssuesData.values) && myIssuesData.values.length > 0) {
+ return this.options.app.state.set({ myIssues: myIssuesData.values[0].count }, { silent: true });
+ } else {
+ return this.options.app.state.unset('myIssues', { silent: true });
+ }
+ },
+
+ fetchList: function (firstPage) {
+ var that = this;
+ if (firstPage == null) {
+ firstPage = true;
+ }
+ if (firstPage) {
+ this.options.app.state.set({ selectedIndex: 0, page: 1 }, { silent: true });
+ this.hideHomePage();
+ this.closeComponentViewer();
+ }
+ var data = this._issuesParameters();
+ _.extend(data, this.options.app.state.get('query'));
+ if (this.options.app.state.get('isContext')) {
+ _.extend(data, this.options.app.state.get('contextQuery'));
+ }
+ return $.get(baseUrl + '/api/issues/search', data).done(function (r) {
+ var issues = that.options.app.list.parseIssues(r);
+ if (firstPage) {
+ that.options.app.list.reset(issues);
+ } else {
+ that.options.app.list.add(issues);
+ }
+ that.options.app.list.setIndex();
+ FACET_DATA_FIELDS.forEach(function (field) {
+ that.options.app.facets[field] = r[field];
+ });
+ that.options.app.facets.reset(that._allFacets());
+ that.options.app.facets.add(_.reject(r.facets, function (f) {
+ return f.property === 'assigned_to_me';
+ }), { merge: true });
+ that._myIssuesFromResponse(r);
+ that.enableFacets(that._enabledFacets());
+ that.options.app.state.set({
+ page: r.p,
+ pageSize: r.ps,
+ total: r.total,
+ maxResultsReached: r.p * r.ps >= r.total
+ });
+ if (firstPage && that.isIssuePermalink()) {
+ return that.showComponentViewer(that.options.app.list.first());
+ }
+ });
+ },
+
+ isIssuePermalink: function () {
+ var query = this.options.app.state.get('query');
+ return (query.issues != null) && this.options.app.list.length === 1;
+ },
+
+ fetchFilters: function () {
+ var that = this;
+ return $.get(baseUrl + '/api/issue_filters/app', function (r) {
+ that.options.app.state.set({
+ canBulkChange: r.canBulkChange,
+ canManageFilters: r.canManageFilters
+ });
+ return that.options.app.filters.reset(r.favorites);
+ });
+ },
+
+ _mergeCollections: function (a, b) {
+ var collection = new Backbone.Collection(a);
+ collection.add(b, { merge: true });
+ return collection.toJSON();
+ },
+
+ requestFacet: function (id) {
+ var that = this;
+ if (id === 'assignees') {
+ return this.requestAssigneeFacet();
+ }
+ var facet = this.options.app.facets.get(id),
+ data = _.extend({ facets: id, ps: 1 }, this.options.app.state.get('query'));
+ if (this.options.app.state.get('isContext')) {
+ _.extend(data, this.options.app.state.get('contextQuery'));
+ }
+ return $.get(baseUrl + '/api/issues/search', data, function (r) {
+ FACET_DATA_FIELDS.forEach(function (field) {
+ that.options.app.facets[field] = that._mergeCollections(that.options.app.facets[field], r[field]);
+ });
+ var facetData = _.findWhere(r.facets, { property: id });
+ if (facetData != null) {
+ return facet.set(facetData);
+ }
+ });
+ },
+
+ requestAssigneeFacet: function () {
+ var that = this;
+ var facet = this.options.app.facets.get('assignees'),
+ data = _.extend({ facets: 'assignees,assigned_to_me', ps: 1 }, this.options.app.state.get('query'));
+ if (this.options.app.state.get('isContext')) {
+ _.extend(data, this.options.app.state.get('contextQuery'));
+ }
+ return $.get(baseUrl + '/api/issues/search', data, function (r) {
+ FACET_DATA_FIELDS.forEach(function (field) {
+ that.options.app.facets[field] = that._mergeCollections(that.options.app.facets[field], r[field]);
+ });
+ var facetData = _.findWhere(r.facets, { property: 'assignees' });
+ that._myIssuesFromResponse(r);
+ if (facetData != null) {
+ return facet.set(facetData);
+ }
+ });
+ },
+
+ newSearch: function () {
+ this.options.app.state.unset('filter');
+ return this.options.app.state.setQuery({ resolved: 'false' });
+ },
+
+ applyFilter: function (filter, ignoreQuery) {
+ if (ignoreQuery == null) {
+ ignoreQuery = false;
+ }
+ if (!ignoreQuery) {
+ var filterQuery = this.parseQuery(filter.get('query'));
+ this.options.app.state.setQuery(filterQuery);
+ }
+ return this.options.app.state.set({ filter: filter, changed: false });
+ },
+
+ parseQuery: function () {
+ var q = Controller.prototype.parseQuery.apply(this, arguments);
+ delete q.asc;
+ delete q.s;
+ return q;
+ },
+
+ getQuery: function (separator, addContext) {
+ if (separator == null) {
+ separator = '|';
+ }
+ if (addContext == null) {
+ addContext = false;
+ }
+ var filter = this.options.app.state.get('query');
+ if (addContext && this.options.app.state.get('isContext')) {
+ _.extend(filter, this.options.app.state.get('contextQuery'));
+ }
+ var route = [];
+ _.map(filter, function (value, property) {
+ return route.push('' + property + '=' + encodeURIComponent(value));
+ });
+ return route.join(separator);
+ },
+
+ getRoute: function () {
+ var filter = this.options.app.state.get('filter'),
+ query = Controller.prototype.getRoute.apply(this, arguments);
+ if (filter != null) {
+ if (this.options.app.state.get('changed') && query.length > 0) {
+ query = 'id=' + filter.id + '|' + query;
+ } else {
+ query = 'id=' + filter.id;
+ }
+ }
+ return query;
+ },
+
+ _prepareComponent: function (issue) {
+ return {
+ key: issue.get('component'),
+ name: issue.get('componentLongName'),
+ qualifier: issue.get('componentQualifier'),
+ project: issue.get('project'),
+ projectName: issue.get('projectLongName')
+ };
+ },
+
+ showComponentViewer: function (issue) {
+ this.options.app.layout.workspaceComponentViewerRegion.reset();
+ key.setScope('componentViewer');
+ this.options.app.issuesView.unbindScrollEvents();
+ this.options.app.state.set('component', this._prepareComponent(issue));
+ this.options.app.componentViewer = new ComponentViewer({ app: this.options.app });
+ this.options.app.layout.workspaceComponentViewerRegion.show(this.options.app.componentViewer);
+ this.options.app.layout.showComponentViewer();
+ return this.options.app.componentViewer.openFileByIssue(issue);
+ },
+
+ closeComponentViewer: function () {
+ key.setScope('list');
+ $('body').click();
+ this.options.app.state.unset('component');
+ this.options.app.layout.workspaceComponentViewerRegion.reset();
+ this.options.app.layout.hideComponentViewer();
+ this.options.app.issuesView.bindScrollEvents();
+ return this.options.app.issuesView.scrollTo();
+ },
+
+ showHomePage: function () {
+ this.fetchList();
+ this.options.app.layout.workspaceComponentViewerRegion.reset();
+ key.setScope('home');
+ this.options.app.issuesView.unbindScrollEvents();
+ this.options.app.homeView = new HomeView({ app: this.options.app });
+ this.options.app.layout.workspaceHomeRegion.show(this.options.app.homeView);
+ return this.options.app.layout.showHomePage();
+ },
+
+ hideHomePage: function () {
+ this.options.app.layout.workspaceComponentViewerRegion.reset();
+ this.options.app.layout.workspaceHomeRegion.reset();
+ key.setScope('list');
+ this.options.app.layout.hideHomePage();
+ this.options.app.issuesView.bindScrollEvents();
+ return this.options.app.issuesView.scrollTo();
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets-view.js b/server/sonar-web/src/main/js/apps/issues/facets-view.js
new file mode 100644
index 00000000000..2409c25b019
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets-view.js
@@ -0,0 +1,65 @@
+define([
+ 'components/navigator/facets-view',
+ './facets/base-facet',
+ './facets/severity-facet',
+ './facets/status-facet',
+ './facets/project-facet',
+ './facets/module-facet',
+ './facets/assignee-facet',
+ './facets/rule-facet',
+ './facets/tag-facet',
+ './facets/resolution-facet',
+ './facets/creation-date-facet',
+ './facets/action-plan-facet',
+ './facets/file-facet',
+ './facets/reporter-facet',
+ './facets/language-facet',
+ './facets/author-facet',
+ './facets/issue-key-facet',
+ './facets/context-facet'
+], function (FacetsView, BaseFacet, SeverityFacet, StatusFacet, ProjectFacet, ModuleFacet, AssigneeFacet, RuleFacet,
+ TagFacet, ResolutionFacet, CreationDateFacet, ActionPlanFacet, FileFacet, ReporterFacet, LanguageFacet,
+ AuthorFacet, IssueKeyFacet, ContextFacet) {
+
+ return FacetsView.extend({
+ getItemView: function (model) {
+ switch (model.get('property')) {
+ case 'severities':
+ return SeverityFacet;
+ case 'statuses':
+ return StatusFacet;
+ case 'assignees':
+ return AssigneeFacet;
+ case 'resolutions':
+ return ResolutionFacet;
+ case 'createdAt':
+ return CreationDateFacet;
+ case 'projectUuids':
+ return ProjectFacet;
+ case 'moduleUuids':
+ return ModuleFacet;
+ case 'rules':
+ return RuleFacet;
+ case 'tags':
+ return TagFacet;
+ case 'actionPlans':
+ return ActionPlanFacet;
+ case 'fileUuids':
+ return FileFacet;
+ case 'reporters':
+ return ReporterFacet;
+ case 'languages':
+ return LanguageFacet;
+ case 'authors':
+ return AuthorFacet;
+ case 'issues':
+ return IssueKeyFacet;
+ case 'context':
+ return ContextFacet;
+ default:
+ return BaseFacet;
+ }
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/action-plan-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/action-plan-facet.js
new file mode 100644
index 00000000000..d85fa9e5dfd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/action-plan-facet.js
@@ -0,0 +1,68 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ var $ = jQuery;
+
+ return BaseFacet.extend({
+ template: Templates['issues-action-plan-facet'],
+
+ onRender: function () {
+ BaseFacet.prototype.onRender.apply(this, arguments);
+ var value = this.options.app.state.get('query').planned;
+ if ((value != null) && (!value || value === 'false')) {
+ return this.$('.js-facet').filter('[data-unplanned]').addClass('active');
+ }
+ },
+
+ toggleFacet: function (e) {
+ var unplanned = $(e.currentTarget).is('[data-unplanned]');
+ $(e.currentTarget).toggleClass('active');
+ if (unplanned) {
+ var checked = $(e.currentTarget).is('.active'),
+ value = checked ? 'false' : null;
+ return this.options.app.state.updateFilter({
+ planned: value,
+ actionPlans: null
+ });
+ } else {
+ return this.options.app.state.updateFilter({
+ planned: null,
+ actionPlans: this.getValue()
+ });
+ }
+ },
+
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ actionPlans = this.options.app.facets.actionPlans;
+ values.forEach(function (v) {
+ var key = v.val,
+ label = null;
+ if (key) {
+ var actionPlan = _.findWhere(actionPlans, { key: key });
+ if (actionPlan != null) {
+ label = actionPlan.name;
+ }
+ }
+ v.label = label;
+ });
+ return values;
+ },
+
+ disable: function () {
+ return this.options.app.state.updateFilter({
+ planned: null,
+ actionPlans: null
+ });
+ },
+
+ serializeData: function () {
+ return _.extend(BaseFacet.prototype.serializeData.apply(this, arguments), {
+ values: this.getValuesWithLabels()
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js
new file mode 100644
index 00000000000..3645b7215d6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/assignee-facet.js
@@ -0,0 +1,108 @@
+define([
+ './custom-values-facet',
+ '../templates'
+], function (CustomValuesFacet) {
+
+ var $ = jQuery;
+
+ return CustomValuesFacet.extend({
+ template: Templates['issues-assignee-facet'],
+
+ getUrl: function () {
+ return baseUrl + '/api/users/search';
+ },
+
+ prepareAjaxSearch: function () {
+ return {
+ quietMillis: 300,
+ url: this.getUrl(),
+ data: function (term, page) {
+ return { q: term, p: page };
+ },
+ results: window.usersToSelect2
+ };
+ },
+
+ onRender: function () {
+ CustomValuesFacet.prototype.onRender.apply(this, arguments);
+ var value = this.options.app.state.get('query').assigned;
+ if ((value != null) && (!value || value === 'false')) {
+ return this.$('.js-facet').filter('[data-unassigned]').addClass('active');
+ }
+ },
+
+ toggleFacet: function (e) {
+ var unassigned = $(e.currentTarget).is('[data-unassigned]');
+ $(e.currentTarget).toggleClass('active');
+ if (unassigned) {
+ var checked = $(e.currentTarget).is('.active'),
+ value = checked ? 'false' : null;
+ return this.options.app.state.updateFilter({
+ assigned: value,
+ assignees: null
+ });
+ } else {
+ return this.options.app.state.updateFilter({
+ assigned: null,
+ assignees: this.getValue()
+ });
+ }
+ },
+
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ users = this.options.app.facets.users;
+ values.forEach(function (v) {
+ var login = v.val,
+ name = '';
+ if (login) {
+ var user = _.findWhere(users, { login: login });
+ if (user != null) {
+ name = user.name;
+ }
+ }
+ v.label = name;
+ });
+ return values;
+ },
+
+ disable: function () {
+ return this.options.app.state.updateFilter({
+ assigned: null,
+ assignees: null
+ });
+ },
+
+ addCustomValue: function () {
+ var property = this.model.get('property'),
+ customValue = this.$('.js-custom-value').select2('val'),
+ value = this.getValue();
+ if (value.length > 0) {
+ value += ',';
+ }
+ value += customValue;
+ var obj = {};
+ obj[property] = value;
+ obj.assigned = null;
+ return this.options.app.state.updateFilter(obj);
+ },
+
+ sortValues: function (values) {
+ return _.sortBy(values, function (v) {
+ return v.val === '' ? -999999 : -v.count;
+ });
+ },
+
+ getNumberOfMyIssues: function () {
+ return this.options.app.state.get('myIssues');
+ },
+
+ serializeData: function () {
+ return _.extend(CustomValuesFacet.prototype.serializeData.apply(this, arguments), {
+ myIssues: this.getNumberOfMyIssues(),
+ values: this.sortValues(this.getValuesWithLabels())
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js
new file mode 100644
index 00000000000..ee987a3d067
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/author-facet.js
@@ -0,0 +1,44 @@
+define([
+ './custom-values-facet'
+], function (CustomValuesFacet) {
+
+ return CustomValuesFacet.extend({
+ getUrl: function () {
+ return baseUrl + '/api/issues/authors';
+ },
+
+ prepareSearch: function () {
+ return this.$('.js-custom-value').select2({
+ placeholder: 'Search...',
+ minimumInputLength: 2,
+ allowClear: false,
+ formatNoMatches: function () {
+ return t('select2.noMatches');
+ },
+ formatSearching: function () {
+ return t('select2.searching');
+ },
+ formatInputTooShort: function () {
+ return tp('select2.tooShort', 2);
+ },
+ width: '100%',
+ ajax: {
+ quietMillis: 300,
+ url: this.getUrl(),
+ data: function (term) {
+ return { q: term, ps: 25 };
+ },
+ results: function (data) {
+ return {
+ more: false,
+ results: data.authors.map(function (author) {
+ return { id: author, text: author };
+ })
+ };
+ }
+ }
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js
new file mode 100644
index 00000000000..1376e8b6611
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/base-facet.js
@@ -0,0 +1,19 @@
+define([
+ 'components/navigator/facets/base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ return BaseFacet.extend({
+ template: Templates['issues-base-facet'],
+
+ onRender: function () {
+ BaseFacet.prototype.onRender.apply(this, arguments);
+ return this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
+ },
+
+ onClose: function () {
+ return this.$('[data-toggle="tooltip"]').tooltip('destroy');
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js
new file mode 100644
index 00000000000..48c88c8f1c0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/context-facet.js
@@ -0,0 +1,16 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ return BaseFacet.extend({
+ template: Templates['issues-context-facet'],
+
+ serializeData: function () {
+ return _.extend(BaseFacet.prototype.serializeData.apply(this, arguments), {
+ state: this.options.app.state.toJSON()
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js
new file mode 100644
index 00000000000..30305f317f1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/creation-date-facet.js
@@ -0,0 +1,131 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ var $ = jQuery;
+
+ return BaseFacet.extend({
+ template: Templates['issues-creation-date-facet'],
+
+ events: function () {
+ return _.extend(BaseFacet.prototype.events.apply(this, arguments), {
+ 'change input': 'applyFacet',
+ 'click .js-select-period-start': 'selectPeriodStart',
+ 'click .js-select-period-end': 'selectPeriodEnd',
+ 'click .sonar-d3 rect': 'selectBar',
+ 'click .js-all': 'onAllClick',
+ 'click .js-last-week': 'onLastWeekClick',
+ 'click .js-last-month': 'onLastMonthClick',
+ 'click .js-last-year': 'onLastYearClick'
+ });
+ },
+
+ onRender: function () {
+ var that = this;
+ this.$el.toggleClass('search-navigator-facet-box-collapsed', !this.model.get('enabled'));
+ this.$('input').datepicker({
+ dateFormat: 'yy-mm-dd',
+ changeMonth: true,
+ changeYear: true
+ });
+ var props = ['createdAfter', 'createdBefore', 'createdAt'],
+ query = this.options.app.state.get('query');
+ props.forEach(function (prop) {
+ var value = query[prop];
+ if (value != null) {
+ return that.$('input[name=' + prop + ']').val(value);
+ }
+ });
+ var values = this.model.getValues();
+ if (!(_.isArray(values) && values.length > 0)) {
+ var date = moment(),
+ i, j;
+ values = [];
+ for (i = j = 0; j <= 10; i = ++j) {
+ values.push({ count: 0, val: date.toDate().toString() });
+ date = date.subtract(1, 'days');
+ }
+ values.reverse();
+ }
+ return this.$('.js-barchart').barchart(values);
+ },
+
+ selectPeriodStart: function () {
+ return this.$('.js-period-start').datepicker('show');
+ },
+
+ selectPeriodEnd: function () {
+ return this.$('.js-period-end').datepicker('show');
+ },
+
+ applyFacet: function () {
+ var obj = { createdAt: null, createdInLast: null };
+ this.$('input').each(function () {
+ var property, value;
+ property = $(this).prop('name');
+ value = $(this).val();
+ obj[property] = value;
+ });
+ return this.options.app.state.updateFilter(obj);
+ },
+
+ disable: function () {
+ return this.options.app.state.updateFilter({
+ createdAfter: null,
+ createdBefore: null,
+ createdAt: null,
+ createdInLast: null
+ });
+ },
+
+ selectBar: function (e) {
+ var periodStart = $(e.currentTarget).data('period-start'),
+ periodEnd = $(e.currentTarget).data('period-end');
+ return this.options.app.state.updateFilter({
+ createdAfter: periodStart,
+ createdBefore: periodEnd,
+ createdAt: null,
+ createdInLast: null
+ });
+ },
+
+ selectPeriod: function (period) {
+ return this.options.app.state.updateFilter({
+ createdAfter: null,
+ createdBefore: null,
+ createdAt: null,
+ createdInLast: period
+ });
+ },
+
+ onAllClick: function () {
+ return this.disable();
+ },
+
+ onLastWeekClick: function (e) {
+ e.preventDefault();
+ return this.selectPeriod('1w');
+ },
+
+ onLastMonthClick: function (e) {
+ e.preventDefault();
+ return this.selectPeriod('1m');
+ },
+
+ onLastYearClick: function (e) {
+ e.preventDefault();
+ return this.selectPeriod('1y');
+ },
+
+ serializeData: function () {
+ return _.extend(BaseFacet.prototype.serializeData.apply(this, arguments), {
+ periodStart: this.options.app.state.get('query').createdAfter,
+ periodEnd: this.options.app.state.get('query').createdBefore,
+ createdAt: this.options.app.state.get('query').createdAt,
+ createdInLast: this.options.app.state.get('query').createdInLast
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js
new file mode 100644
index 00000000000..11b3aebdd74
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/custom-values-facet.js
@@ -0,0 +1,70 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ return BaseFacet.extend({
+ template: Templates['issues-custom-values-facet'],
+
+ events: function () {
+ return _.extend(BaseFacet.prototype.events.apply(this, arguments), {
+ 'change .js-custom-value': 'addCustomValue'
+ });
+ },
+
+ getUrl: function () {
+
+ },
+
+ onRender: function () {
+ BaseFacet.prototype.onRender.apply(this, arguments);
+ return this.prepareSearch();
+ },
+
+ prepareSearch: function () {
+ return this.$('.js-custom-value').select2({
+ placeholder: 'Search...',
+ minimumInputLength: 2,
+ allowClear: false,
+ formatNoMatches: function () {
+ return t('select2.noMatches');
+ },
+ formatSearching: function () {
+ return t('select2.searching');
+ },
+ formatInputTooShort: function () {
+ return tp('select2.tooShort', 2);
+ },
+ width: '100%',
+ ajax: this.prepareAjaxSearch()
+ });
+ },
+
+ prepareAjaxSearch: function () {
+ return {
+ quietMillis: 300,
+ url: this.getUrl(),
+ data: function (term, page) {
+ return { s: term, p: page };
+ },
+ results: function (data) {
+ return { more: data.more, results: data.results };
+ }
+ };
+ },
+
+ addCustomValue: function () {
+ var property = this.model.get('property'),
+ customValue = this.$('.js-custom-value').select2('val'),
+ value = this.getValue();
+ if (value.length > 0) {
+ value += ',';
+ }
+ value += customValue;
+ var obj = {};
+ obj[property] = value;
+ return this.options.app.state.updateFilter(obj);
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js
new file mode 100644
index 00000000000..d01340e6d3c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/file-facet.js
@@ -0,0 +1,43 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ var $ = jQuery;
+
+ return BaseFacet.extend({
+ template: Templates['issues-file-facet'],
+
+ onRender: function () {
+ BaseFacet.prototype.onRender.apply(this, arguments);
+ var maxValueWidth = _.max(this.$('.facet-stat').map(function () {
+ return $(this).outerWidth();
+ }).get());
+ return this.$('.facet-name').css('padding-right', maxValueWidth);
+ },
+
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ source = this.options.app.facets.components;
+ values.forEach(function (v) {
+ var key = v.val,
+ label = null;
+ if (key) {
+ var item = _.findWhere(source, { uuid: key });
+ if (item != null) {
+ label = item.longName;
+ }
+ }
+ v.label = label;
+ });
+ return values;
+ },
+
+ serializeData: function () {
+ return _.extend(BaseFacet.prototype.serializeData.apply(this, arguments), {
+ values: this.sortValues(this.getValuesWithLabels())
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js
new file mode 100644
index 00000000000..2d309b85a3e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/issue-key-facet.js
@@ -0,0 +1,24 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ return BaseFacet.extend({
+ template: Templates['issues-issue-key-facet'],
+
+ onRender: function () {
+ return this.$el.toggleClass('hidden', !this.options.app.state.get('query').issues);
+ },
+
+ disable: function () {
+ return this.options.app.state.updateFilter({ issues: null });
+ },
+
+ serializeData: function () {
+ return _.extend(BaseFacet.prototype.serializeData.apply(this, arguments), {
+ issues: this.options.app.state.get('query').issues
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js
new file mode 100644
index 00000000000..3418158fba4
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/language-facet.js
@@ -0,0 +1,67 @@
+define([
+ './custom-values-facet'
+], function (CustomValuesFacet) {
+
+ return CustomValuesFacet.extend({
+ getUrl: function () {
+ return baseUrl + '/api/languages/list';
+ },
+
+ prepareSearch: function () {
+ return this.$('.js-custom-value').select2({
+ placeholder: 'Search...',
+ minimumInputLength: 2,
+ allowClear: false,
+ formatNoMatches: function () {
+ return t('select2.noMatches');
+ },
+ formatSearching: function () {
+ return t('select2.searching');
+ },
+ formatInputTooShort: function () {
+ return tp('select2.tooShort', 2);
+ },
+ width: '100%',
+ ajax: {
+ quietMillis: 300,
+ url: this.getUrl(),
+ data: function (term) {
+ return { q: term, ps: 0 };
+ },
+ results: function (data) {
+ return {
+ more: false,
+ results: data.languages.map(function (lang) {
+ return { id: lang.key, text: lang.name };
+ })
+ };
+ }
+ }
+ });
+ },
+
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ source = this.options.app.facets.languages;
+ values.forEach(function (v) {
+ var key = v.val,
+ label = null;
+ if (key) {
+ var item = _.findWhere(source, { key: key });
+ if (item != null) {
+ label = item.name;
+ }
+ }
+ v.label = label;
+ });
+ return values;
+ },
+
+ serializeData: function () {
+ return _.extend(CustomValuesFacet.prototype.serializeData.apply(this, arguments), {
+ values: this.sortValues(this.getValuesWithLabels())
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js
new file mode 100644
index 00000000000..96c4a1de07f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/module-facet.js
@@ -0,0 +1,30 @@
+define([
+ './base-facet'
+], function (BaseFacet) {
+
+ return BaseFacet.extend({
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ components = this.options.app.facets.components;
+ values.forEach(function (v) {
+ var uuid = v.val,
+ label = uuid;
+ if (uuid) {
+ var component = _.findWhere(components, { uuid: uuid });
+ if (component != null) {
+ label = component.longName;
+ }
+ }
+ v.label = label;
+ });
+ return values;
+ },
+
+ serializeData: function () {
+ return _.extend(BaseFacet.prototype.serializeData.apply(this, arguments), {
+ values: this.sortValues(this.getValuesWithLabels())
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js
new file mode 100644
index 00000000000..ad7126b427d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/project-facet.js
@@ -0,0 +1,83 @@
+define([
+ './custom-values-facet'
+], function (CustomValuesFacet) {
+
+ return CustomValuesFacet.extend({
+
+ getUrl: function () {
+ var q = this.options.app.state.get('contextComponentQualifier');
+ if (q === 'VW' || q === 'SVW') {
+ return baseUrl + '/api/components/search';
+ } else {
+ return baseUrl + '/api/resources/search?f=s2&q=TRK&display_uuid=true';
+ }
+ },
+
+ prepareSearch: function () {
+ var q = this.options.app.state.get('contextComponentQualifier');
+ if (q === 'VW' || q === 'SVW') {
+ return this.prepareSearchForViews();
+ } else {
+ return CustomValuesFacet.prototype.prepareSearch.apply(this, arguments);
+ }
+ },
+
+ prepareSearchForViews: function () {
+ var componentUuid = this.options.app.state.get('contextComponentUuid');
+ return this.$('.js-custom-value').select2({
+ placeholder: 'Search...',
+ minimumInputLength: 2,
+ allowClear: false,
+ formatNoMatches: function () {
+ return t('select2.noMatches');
+ },
+ formatSearching: function () {
+ return t('select2.searching');
+ },
+ formatInputTooShort: function () {
+ return tp('select2.tooShort', 2);
+ },
+ width: '100%',
+ ajax: {
+ quietMillis: 300,
+ url: this.getUrl(),
+ data: function (term, page) {
+ return { q: term, componentUuid: componentUuid, p: page, ps: 25 };
+ },
+ results: function (data) {
+ return {
+ more: data.p * data.ps < data.total,
+ results: data.components.map(function (c) {
+ return { id: c.uuid, text: c.name };
+ })
+ };
+ }
+ }
+ });
+ },
+
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ projects = this.options.app.facets.projects;
+ values.forEach(function (v) {
+ var uuid = v.val,
+ label = '';
+ if (uuid) {
+ var project = _.findWhere(projects, { uuid: uuid });
+ if (project != null) {
+ label = project.longName;
+ }
+ }
+ v.label = label;
+ });
+ return values;
+ },
+
+ serializeData: function () {
+ return _.extend(CustomValuesFacet.prototype.serializeData.apply(this, arguments), {
+ values: this.sortValues(this.getValuesWithLabels())
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js
new file mode 100644
index 00000000000..6340aefe04f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/reporter-facet.js
@@ -0,0 +1,46 @@
+define([
+ './custom-values-facet'
+], function (CustomValuesFacet) {
+
+ return CustomValuesFacet.extend({
+ getUrl: function () {
+ return baseUrl + '/api/users/search';
+ },
+
+ prepareAjaxSearch: function () {
+ return {
+ quietMillis: 300,
+ url: this.getUrl(),
+ data: function (term, page) {
+ return { q: term, p: page };
+ },
+ results: window.usersToSelect2
+ };
+ },
+
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ source = this.options.app.facets.users;
+ values.forEach(function (v) {
+ var item, key, label;
+ key = v.val;
+ label = null;
+ if (key) {
+ item = _.findWhere(source, { login: key });
+ if (item != null) {
+ label = item.name;
+ }
+ }
+ v.label = label;
+ });
+ return values;
+ },
+
+ serializeData: function () {
+ return _.extend(CustomValuesFacet.prototype.serializeData.apply(this, arguments), {
+ values: this.sortValues(this.getValuesWithLabels())
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js
new file mode 100644
index 00000000000..75dd3e78289
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/resolution-facet.js
@@ -0,0 +1,52 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ var $ = jQuery;
+
+ return BaseFacet.extend({
+ template: Templates['issues-resolution-facet'],
+
+ onRender: function () {
+ BaseFacet.prototype.onRender.apply(this, arguments);
+ var value = this.options.app.state.get('query').resolved;
+ if ((value != null) && (!value || value === 'false')) {
+ return this.$('.js-facet').filter('[data-unresolved]').addClass('active');
+ }
+ },
+
+ toggleFacet: function (e) {
+ var unresolved = $(e.currentTarget).is('[data-unresolved]');
+ $(e.currentTarget).toggleClass('active');
+ if (unresolved) {
+ var checked = $(e.currentTarget).is('.active'),
+ value = checked ? 'false' : null;
+ return this.options.app.state.updateFilter({
+ resolved: value,
+ resolutions: null
+ });
+ } else {
+ return this.options.app.state.updateFilter({
+ resolved: null,
+ resolutions: this.getValue()
+ });
+ }
+ },
+
+ disable: function () {
+ return this.options.app.state.updateFilter({
+ resolved: null,
+ resolutions: null
+ });
+ },
+
+ sortValues: function (values) {
+ var order = ['', 'FIXED', 'FALSE-POSITIVE', 'WONTFIX', 'REMOVED'];
+ return _.sortBy(values, function (v) {
+ return order.indexOf(v.val);
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js
new file mode 100644
index 00000000000..569fcf6c7de
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/rule-facet.js
@@ -0,0 +1,78 @@
+define([
+ './custom-values-facet'
+], function (CustomValuesFacet) {
+
+ return CustomValuesFacet.extend({
+ prepareSearch: function () {
+ var url = baseUrl + '/api/rules/search?f=name,langName',
+ languages = this.options.app.state.get('query').languages;
+ if (languages != null) {
+ url += '&languages=' + languages;
+ }
+ return this.$('.js-custom-value').select2({
+ placeholder: 'Search...',
+ minimumInputLength: 2,
+ allowClear: false,
+ formatNoMatches: function () {
+ return t('select2.noMatches');
+ },
+ formatSearching: function () {
+ return t('select2.searching');
+ },
+ formatInputTooShort: function () {
+ return tp('select2.tooShort', 2);
+ },
+ width: '100%',
+ ajax: {
+ quietMillis: 300,
+ url: url,
+ data: function (term, page) {
+ return { q: term, p: page };
+ },
+ results: function (data) {
+ var results;
+ results = data.rules.map(function (rule) {
+ return {
+ id: rule.key,
+ text: '(' + rule.langName + ') ' + rule.name
+ };
+ });
+ return {
+ more: data.p * data.ps < data.total,
+ results: results
+ };
+ }
+ }
+ });
+ },
+
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ rules = this.options.app.facets.rules;
+ values.forEach(function (v) {
+ var key = v.val,
+ label = '',
+ extra = '';
+ if (key) {
+ var rule = _.findWhere(rules, { key: key });
+ if (rule != null) {
+ label = rule.name;
+ }
+ if (rule != null) {
+ extra = rule.langName;
+ }
+ }
+ v.label = label;
+ v.extra = extra;
+ });
+ return values;
+ },
+
+ serializeData: function () {
+ return _.extend(CustomValuesFacet.prototype.serializeData.apply(this, arguments), {
+ values: this.sortValues(this.getValuesWithLabels())
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js
new file mode 100644
index 00000000000..eccaf546684
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/severity-facet.js
@@ -0,0 +1,17 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ return BaseFacet.extend({
+ template: Templates['issues-severity-facet'],
+
+ sortValues: function (values) {
+ var order = ['BLOCKER', 'MINOR', 'CRITICAL', 'INFO', 'MAJOR'];
+ return _.sortBy(values, function (v) {
+ return order.indexOf(v.val);
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js
new file mode 100644
index 00000000000..08db6d508f8
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/status-facet.js
@@ -0,0 +1,17 @@
+define([
+ './base-facet',
+ '../templates'
+], function (BaseFacet) {
+
+ return BaseFacet.extend({
+ template: Templates['issues-status-facet'],
+
+ sortValues: function (values) {
+ var order = ['OPEN', 'RESOLVED', 'REOPENED', 'CLOSED', 'CONFIRMED'];
+ return _.sortBy(values, function (v) {
+ return order.indexOf(v.val);
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js b/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js
new file mode 100644
index 00000000000..7752451272b
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/facets/tag-facet.js
@@ -0,0 +1,56 @@
+define([
+ './custom-values-facet'
+], function (CustomValuesFacet) {
+
+ return CustomValuesFacet.extend({
+ prepareSearch: function () {
+ var url = baseUrl + '/api/issues/tags?ps=10',
+ tags = this.options.app.state.get('query').tags;
+ if (tags != null) {
+ url += '&tags=' + tags;
+ }
+ return this.$('.js-custom-value').select2({
+ placeholder: 'Search...',
+ minimumInputLength: 0,
+ allowClear: false,
+ formatNoMatches: function () {
+ return t('select2.noMatches');
+ },
+ formatSearching: function () {
+ return t('select2.searching');
+ },
+ width: '100%',
+ ajax: {
+ quietMillis: 300,
+ url: url,
+ data: function (term) {
+ return { q: term, ps: 10 };
+ },
+ results: function (data) {
+ var results = data.tags.map(function (tag) {
+ return { id: tag, text: tag };
+ });
+ return { more: false, results: results };
+ }
+ }
+ });
+ },
+
+ getValuesWithLabels: function () {
+ var values = this.model.getValues(),
+ tags = this.options.app.facets.tags;
+ values.forEach(function (v) {
+ v.label = v.val;
+ v.extra = '';
+ });
+ return values;
+ },
+
+ serializeData: function () {
+ return _.extend(CustomValuesFacet.prototype.serializeData.apply(this, arguments), {
+ values: this.sortValues(this.getValuesWithLabels())
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/filters-view.js b/server/sonar-web/src/main/js/apps/issues/filters-view.js
new file mode 100644
index 00000000000..03813eff3c0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/filters-view.js
@@ -0,0 +1,93 @@
+define([
+ './templates'
+], function () {
+
+ var $ = jQuery;
+
+ return Marionette.ItemView.extend({
+ template: Templates['issues-filters'],
+
+ events: {
+ 'click .js-toggle-filters': 'toggleFilters',
+ 'click .js-filter': 'applyFilter',
+ 'click .js-filter-save-as': 'saveAs',
+ 'click .js-filter-save': 'save',
+ 'click .js-filter-copy': 'copy',
+ 'click .js-filter-edit': 'edit'
+ },
+
+ initialize: function (options) {
+ var that = this;
+ this.listenTo(options.app.state, 'change:filter', this.render);
+ this.listenTo(options.app.state, 'change:changed', this.render);
+ this.listenTo(options.app.filters, 'all', this.render);
+ window.onSaveAs = window.onCopy = window.onEdit = function (id) {
+ $('#modal').dialog('close');
+ return that.options.app.controller.fetchFilters().done(function () {
+ var filter = that.collection.get(id);
+ return filter.fetch().done(function () {
+ return that.options.app.controller.applyFilter(filter);
+ });
+ });
+ };
+ },
+
+ onRender: function () {
+ this.$el.toggleClass('search-navigator-filters-selected', this.options.app.state.has('filter'));
+ },
+
+ toggleFilters: function (e) {
+ var that = this;
+ e.stopPropagation();
+ this.$('.search-navigator-filters-list').toggle();
+ return $('body').on('click.issues-filters', function () {
+ $('body').off('click.issues-filters');
+ return that.$('.search-navigator-filters-list').hide();
+ });
+ },
+
+ applyFilter: function (e) {
+ var that = this;
+ var id = $(e.currentTarget).data('id'),
+ filter = this.collection.get(id);
+ return filter.fetch().done(function () {
+ return that.options.app.controller.applyFilter(filter);
+ });
+ },
+
+ saveAs: function () {
+ var query = this.options.app.controller.getQuery('&'),
+ url = baseUrl + '/issues/save_as_form?' + query;
+ window.openModalWindow(url, {});
+ },
+
+ save: function () {
+ var that = this;
+ var query = this.options.app.controller.getQuery('&'),
+ url = baseUrl + '/issues/save/' + (this.options.app.state.get('filter').id) + '?' + query;
+ return $.post(url).done(function () {
+ return that.options.app.state.set({ changed: false });
+ });
+ },
+
+ copy: function () {
+ var url = baseUrl + '/issues/copy_form/' + (this.options.app.state.get('filter').id);
+ window.openModalWindow(url, {});
+ },
+
+ edit: function () {
+ var url = baseUrl + '/issues/edit_form/' + (this.options.app.state.get('filter').id);
+ window.openModalWindow(url, {});
+ },
+
+ serializeData: function () {
+ var filter = this.options.app.state.get('filter');
+ return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
+ state: this.options.app.state.toJSON(),
+ filter: filter != null ? filter.toJSON() : null,
+ currentUser: window.SS.user
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js b/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js
new file mode 100644
index 00000000000..67f9a617d45
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/issue-filter-view.js
@@ -0,0 +1,29 @@
+define([
+ 'components/issue/views/action-options-view',
+ './templates'
+], function (ActionOptionsView) {
+
+ var $ = jQuery;
+
+ return ActionOptionsView.extend({
+ template: Templates['issues-issue-filter-form'],
+
+ selectInitialOption: function () {
+ return this.makeActive(this.getOptions().first());
+ },
+
+ selectOption: function (e) {
+ var property = $(e.currentTarget).data('property'),
+ value = $(e.currentTarget).data('value');
+ this.trigger('select', property, value);
+ return ActionOptionsView.prototype.selectOption.apply(this, arguments);
+ },
+
+ serializeData: function () {
+ return _.extend(ActionOptionsView.prototype.serializeData.apply(this, arguments), {
+ s: this.model.get('severity')
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/layout.js b/server/sonar-web/src/main/js/apps/issues/layout.js
new file mode 100644
index 00000000000..06b2f893af6
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/layout.js
@@ -0,0 +1,55 @@
+define([
+ './templates'
+], function () {
+
+ var $ = jQuery;
+ return Marionette.Layout.extend({
+ template: Templates['issues-layout'],
+
+ regions: {
+ filtersRegion: '.search-navigator-filters',
+ facetsRegion: '.search-navigator-facets',
+ workspaceHeaderRegion: '.search-navigator-workspace-header',
+ workspaceListRegion: '.search-navigator-workspace-list',
+ workspaceComponentViewerRegion: '.issues-workspace-component-viewer',
+ workspaceHomeRegion: '.issues-workspace-home'
+ },
+
+ onRender: function () {
+ if (this.options.app.state.get('isContext')) {
+ this.$(this.filtersRegion.el).addClass('hidden');
+ }
+ $('.search-navigator').addClass('sticky');
+ var top = $('.search-navigator').offset().top;
+ this.$('.search-navigator-workspace-header').css({ top: top });
+ this.$('.search-navigator-side').css({ top: top }).isolatedScroll();
+ },
+
+ showSpinner: function (region) {
+ return this[region].show(new Marionette.ItemView({
+ template: _.template('<i class="spinner"></i>')
+ }));
+ },
+
+ showComponentViewer: function () {
+ this.scroll = $(window).scrollTop();
+ $('.issues').addClass('issues-extended-view');
+ },
+
+ hideComponentViewer: function () {
+ $('.issues').removeClass('issues-extended-view');
+ if (this.scroll != null) {
+ $(window).scrollTop(this.scroll);
+ }
+ },
+
+ showHomePage: function () {
+ $('.issues').addClass('issues-home-view');
+ },
+
+ hideHomePage: function () {
+ $('.issues').removeClass('issues-home-view');
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/facet.js b/server/sonar-web/src/main/js/apps/issues/models/facet.js
new file mode 100644
index 00000000000..3716ba52d19
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/models/facet.js
@@ -0,0 +1,20 @@
+define(function () {
+
+ return Backbone.Model.extend({
+ idAttribute: 'property',
+
+ defaults: {
+ enabled: false
+ },
+
+ getValues: function () {
+ return this.get('values') || [];
+ },
+
+ toggle: function () {
+ var enabled = this.get('enabled');
+ return this.set({ enabled: !enabled });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/facets.js b/server/sonar-web/src/main/js/apps/issues/models/facets.js
new file mode 100644
index 00000000000..31c685de08d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/models/facets.js
@@ -0,0 +1,9 @@
+define([
+ './facet'
+], function (Facet) {
+
+ return Backbone.Collection.extend({
+ model: Facet
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/filter.js b/server/sonar-web/src/main/js/apps/issues/models/filter.js
new file mode 100644
index 00000000000..42b11507ee1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/models/filter.js
@@ -0,0 +1,17 @@
+define(function () {
+
+ return Backbone.Model.extend({
+ url: function () {
+ return '/api/issue_filters/show/' + this.id;
+ },
+
+ parse: function (r) {
+ if (r.filter != null) {
+ return r.filter;
+ } else {
+ return r;
+ }
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/filters.js b/server/sonar-web/src/main/js/apps/issues/models/filters.js
new file mode 100644
index 00000000000..bb66327e423
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/models/filters.js
@@ -0,0 +1,9 @@
+define([
+ './filter'
+], function (Filter) {
+
+ return Backbone.Collection.extend({
+ model: Filter
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/issues.js b/server/sonar-web/src/main/js/apps/issues/models/issues.js
new file mode 100644
index 00000000000..af41dd3fe12
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/models/issues.js
@@ -0,0 +1,65 @@
+define([
+ 'components/issue/models/issue'
+], function (Issue) {
+
+ return Backbone.Collection.extend({
+ model: Issue,
+
+ url: function () {
+ return baseUrl + '/api/issues/search';
+ },
+
+ parseIssues: function (r) {
+ var find = function (source, key, keyField) {
+ var searchDict = {};
+ searchDict[keyField || 'key'] = key;
+ return _.findWhere(source, searchDict) || key;
+ };
+ return r.issues.map(function (issue, index) {
+ var component = find(r.components, issue.component),
+ project = find(r.projects, issue.project),
+ subProject = find(r.components, issue.subProject),
+ rule = find(r.rules, issue.rule),
+ assignee = find(r.users, issue.assignee, 'login');
+ _.extend(issue, { index: index });
+ if (component) {
+ _.extend(issue, {
+ componentUuid: component.uuid,
+ componentLongName: component.longName,
+ componentQualifier: component.qualifier
+ });
+ }
+ if (project) {
+ _.extend(issue, {
+ projectLongName: project.longName,
+ projectUuid: project.uuid
+ });
+ }
+ if (subProject) {
+ _.extend(issue, {
+ subProjectLongName: subProject.longName,
+ subProjectUuid: subProject.uuid
+ });
+ }
+ if (rule) {
+ _.extend(issue, {
+ ruleName: rule.name
+ });
+ }
+ if (assignee) {
+ _.extend(issue, {
+ assigneeEmail: assignee.email
+ });
+ }
+ return issue;
+ });
+ },
+
+ setIndex: function () {
+ return this.forEach(function (issue, index) {
+ return issue.set({ index: index });
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/models/state.js b/server/sonar-web/src/main/js/apps/issues/models/state.js
new file mode 100644
index 00000000000..38814d71111
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/models/state.js
@@ -0,0 +1,58 @@
+define([
+ 'components/navigator/models/state'
+], function (State) {
+
+ return State.extend({
+ defaults: {
+ page: 1,
+ maxResultsReached: false,
+ query: {},
+ facets: ['severities', 'resolutions'],
+ isContext: false,
+ allFacets: [
+ 'issues',
+ 'severities',
+ 'resolutions',
+ 'statuses',
+ 'createdAt',
+ 'rules',
+ 'tags',
+ 'projectUuids',
+ 'moduleUuids',
+ 'directories',
+ 'fileUuids',
+ 'assignees',
+ 'reporters',
+ 'authors',
+ 'languages',
+ 'actionPlans'
+ ],
+ facetsFromServer: [
+ 'severities',
+ 'statuses',
+ 'resolutions',
+ 'actionPlans',
+ 'projectUuids',
+ 'directories',
+ 'rules',
+ 'moduleUuids',
+ 'tags',
+ 'assignees',
+ 'reporters',
+ 'authors',
+ 'fileUuids',
+ 'languages',
+ 'createdAt'
+ ],
+ transform: {
+ 'resolved': 'resolutions',
+ 'assigned': 'assignees',
+ 'planned': 'actionPlans',
+ 'createdBefore': 'createdAt',
+ 'createdAfter': 'createdAt',
+ 'createdInLast': 'createdAt'
+ }
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/router.js b/server/sonar-web/src/main/js/apps/issues/router.js
new file mode 100644
index 00000000000..519689e8f40
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/router.js
@@ -0,0 +1,45 @@
+define([
+ 'components/navigator/router'
+], function (Router) {
+
+ return Router.extend({
+ routes: {
+ '': 'home',
+ ':query': 'index'
+ },
+
+ initialize: function (options) {
+ Router.prototype.initialize.apply(this, arguments);
+ this.listenTo(options.app.state, 'change:filter', this.updateRoute);
+ },
+
+ home: function () {
+ if (this.options.app.state.get('isContext')) {
+ return this.navigate('resolved=false', { trigger: true, replace: true });
+ } else {
+ return this.options.app.controller.showHomePage();
+ }
+ },
+
+ index: function (query) {
+ var that = this;
+ query = this.options.app.controller.parseQuery(query);
+ if (query.id != null) {
+ var filter = this.options.app.filters.get(query.id);
+ delete query.id;
+ return filter.fetch().done(function () {
+ if (Object.keys(query).length > 0) {
+ that.options.app.controller.applyFilter(filter, true);
+ that.options.app.state.setQuery(query);
+ that.options.app.state.set({ changed: true });
+ } else {
+ that.options.app.controller.applyFilter(filter);
+ }
+ });
+ } else {
+ return this.options.app.state.setQuery(query);
+ }
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/_issues-filter-name.hbs b/server/sonar-web/src/main/js/apps/issues/templates/_issues-filter-name.hbs
new file mode 100644
index 00000000000..22b760eac1a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/_issues-filter-name.hbs
@@ -0,0 +1,18 @@
+{{#if filter.name}}
+ {{filter.name}}
+ <span class='note nowrap'>
+ {{#unless filter.shared}}
+ [{{t 'issue_filter.private'}}]
+ {{else}}
+ {{#eq filter.user currentUser}}
+ [{{t 'issue_filter.shared_with_all_users'}}]
+ {{else}}
+ {{#if filter.user}}
+ [{{t 'issue_filter.shared'}}]
+ {{/if}}
+ {{/eq}}
+ {{/unless}}
+ </span>
+{{else}}
+ {{t 'issues'}}
+{{/if}}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs
new file mode 100644
index 00000000000..8e012d1fd67
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/_issues-facet-header.hbs
@@ -0,0 +1,4 @@
+<a class='search-navigator-facet-header js-facet-toggle'>
+ <i class='icon-checkbox {{#if enabled}}icon-checkbox-checked{{/if}}'></i>
+ {{t 'issues.facet' property}}
+</a>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-action-plan-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-action-plan-facet.hbs
new file mode 100644
index 00000000000..e934608d214
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-action-plan-facet.hbs
@@ -0,0 +1,18 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-list'>
+ {{#each values}}
+ {{#eq val ''}}
+ {{! unplanned }}
+ <a class='facet search-navigator-facet js-facet' data-unplanned title='{{t 'issue.unplanned'}}'>
+ <span class='facet-name'>{{t 'issue.unplanned'}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{else}}
+ <a class='facet search-navigator-facet js-facet' data-value='{{val}}' title='{{label}}'>
+ <span class='facet-name'>{{label}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{/eq}}
+ {{/each}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs
new file mode 100644
index 00000000000..44d4b341f72
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-assignee-facet.hbs
@@ -0,0 +1,30 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-list'>
+ {{#notNull myIssues}}
+ <a class='facet search-navigator-facet js-facet' data-value='__me__' title='{{t 'me'}}'>
+ <span class='facet-name'>{{t 'me'}}</span>
+ <span class='facet-stat'>{{myIssues}}</span>
+ </a>
+ <hr>
+ {{/notNull}}
+
+ {{#each values}}
+ {{#eq val ''}}
+ {{! unassigned }}
+ <a class='facet search-navigator-facet js-facet' data-unassigned title='{{t 'unassigned'}}'>
+ <span class='facet-name'>{{t 'unassigned'}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{else}}
+ <a class='facet search-navigator-facet js-facet' data-value='{{val}}' title='{{label}}'>
+ <span class='facet-name'>{{label}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{/eq}}
+ {{/each}}
+
+ <div class='search-navigator-facet-custom-value'>
+ <input type='hidden' class='js-custom-value'>
+ </div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs
new file mode 100644
index 00000000000..d8b5f0b0179
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-base-facet.hbs
@@ -0,0 +1,10 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-list'>
+ {{#each values}}
+ <a class='facet search-navigator-facet js-facet' data-value='{{val}}' title='{{default label val}}'>
+ <span class='facet-name'>{{default label val}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{/each}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs
new file mode 100644
index 00000000000..5c570f8718c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-context-facet.hbs
@@ -0,0 +1,3 @@
+<div class='search-navigator-facet-query'>
+ Issues of &nbsp;&nbsp; {{qualifierIcon state.contextComponentQualifier}}&nbsp;{{state.contextComponentName}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs
new file mode 100644
index 00000000000..9003a170522
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-creation-date-facet.hbs
@@ -0,0 +1,32 @@
+{{> '_issues-facet-header'}}
+
+{{#if createdAt}}
+ <input type='hidden' name='createdAt'>
+ <div class='search-navigator-facet-container'>
+ {{dt createdAt}} ({{fromNow createdAt}})
+ </div>
+{{else}}
+ <div class='search-navigator-facet-container'>
+ <div class='js-barchart' data-height='75' {{#if periodEnd}}data-end-date='{{periodEnd}}'{{/if}}></div>
+ <div class='search-navigator-date-facet-selection'>
+ <a class='js-select-period-start search-navigator-date-facet-selection-dropdown-left'>
+ {{#if periodStart}}{{d periodStart}}{{else}}Past{{/if}}&nbsp;<i class='icon-dropdown'></i>
+ </a>
+ <a class='js-select-period-end search-navigator-date-facet-selection-dropdown-right'>
+ {{#if periodEnd}}{{d periodEnd}}{{else}}Now{{/if}}&nbsp;<i class='icon-dropdown'></i>
+ </a>
+ <input class='js-period-start search-navigator-date-facet-selection-input-left'
+ type='text' value='{{#if periodStart}}{{ds periodStart}}{{/if}}' name='createdAfter'>
+ <input class='js-period-end search-navigator-date-facet-selection-input-right'
+ type='text' value='{{#if periodEnd}}{{ds periodEnd}}{{/if}}' name='createdBefore'>
+ </div>
+
+ <div class='spacer-top'>
+ <span class='spacer-right'>{{t 'issues.facet.createdAt.or'}}</span>
+ <a class='js-all spacer-right' href='#'>{{t 'issues.facet.createdAt.all'}}</a>
+ <a class='js-last-week spacer-right {{#eq createdInLast '1w'}}active-link{{/eq}}' href='#'>{{t 'issues.facet.createdAt.last_week'}}</a>
+ <a class='js-last-month spacer-right {{#eq createdInLast '1m'}}active-link{{/eq}}' href='#'>{{t 'issues.facet.createdAt.last_month'}}</a>
+ <a class='js-last-year {{#eq createdInLast '1y'}}active-link{{/eq}}' href='#'>{{t 'issues.facet.createdAt.last_year'}}</a>
+ </div>
+ </div>
+{{/if}}
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs
new file mode 100644
index 00000000000..30372e84ae0
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-custom-values-facet.hbs
@@ -0,0 +1,14 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-list'>
+ {{#each values}}
+ <a class='facet search-navigator-facet js-facet' data-value='{{val}}' title='{{#if extra}}({{extra}}) {{/if}}{{default label val}}'>
+ <span class='facet-name'>{{default label val}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{/each}}
+
+ <div class='search-navigator-facet-custom-value'>
+ <input type='hidden' class='js-custom-value'>
+ </div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs
new file mode 100644
index 00000000000..c070fb7785a
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-file-facet.hbs
@@ -0,0 +1,10 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-list search-navigator-facet-list-align-right'>
+ {{#each values}}
+ <a class='facet search-navigator-facet js-facet' data-value='{{val}}' title='{{default label val}}'>
+ <span class='facet-name'>{{default label val}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{/each}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs
new file mode 100644
index 00000000000..540e9a00014
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-issue-key-facet.hbs
@@ -0,0 +1,7 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-container'>
+ <div class='facet search-navigator-facet active' style='cursor: default;'>
+ <span class='facet-name'>{{issues}}</span>
+ </div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs
new file mode 100644
index 00000000000..6fb81c5548f
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-resolution-facet.hbs
@@ -0,0 +1,20 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-list'>
+ {{#each values}}
+ {{#eq val ''}}
+ {{! unresolved }}
+ <a class='facet search-navigator-facet search-navigator-facet-half js-facet' data-unresolved
+ title='{{t 'issue.unresolved.description'}}' data-toggle='tooltip' data-placement='right'>
+ <span class='facet-name'>{{t 'unresolved'}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{else}}
+ <a class='facet search-navigator-facet search-navigator-facet-half js-facet' data-value='{{val}}'
+ title='{{t 'issue.resolution' val 'description'}}' data-toggle='tooltip' data-placement='right'>
+ <span class='facet-name'>{{t 'issue.resolution' val}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{/eq}}
+ {{/each}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs
new file mode 100644
index 00000000000..92ff7b52444
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-severity-facet.hbs
@@ -0,0 +1,11 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-list'>
+ {{#each values}}
+ <a class='facet search-navigator-facet search-navigator-facet-half js-facet'
+ data-value='{{val}}' title='{{t 'severity' val 'description'}}' data-toggle='tooltip' data-placement='right'>
+ <span class='facet-name'>{{severityIcon val}} {{t 'severity' val}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{/each}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs
new file mode 100644
index 00000000000..55a78a63bf1
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/facets/issues-status-facet.hbs
@@ -0,0 +1,11 @@
+{{> '_issues-facet-header'}}
+
+<div class='search-navigator-facet-list'>
+ {{#each values}}
+ <a class='facet search-navigator-facet search-navigator-facet-half js-facet'
+ data-value='{{val}}' title='{{t 'issue.status' val 'description'}}' data-toggle='tooltip' data-placement='right'>
+ <span class='facet-name'>{{statusIcon val}} {{t 'issue.status' val}}</span>
+ <span class='facet-stat'>{{numberShort count}}</span>
+ </a>
+ {{/each}}
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-filters.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-filters.hbs
new file mode 100644
index 00000000000..e010deb9a0e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-filters.hbs
@@ -0,0 +1,55 @@
+<h1 class='page-title dropdown'>
+ {{#if state.canManageFilters}}
+ <a class='search-navigator-filters-show-list dropdown-toggle' data-toggle='dropdown'>
+ <i class='icon-list'></i><span class='issues-filters-name'>{{> '_issues-filter-name'}}</span>
+ </a>
+ <ul class='dropdown-menu'>
+ {{#each items}}
+ <li>
+ <a class='search-navigator-filters-button search-navigator-filters-filter js-filter' data-id='{{id}}'>
+ {{name}}
+ </a>
+ </li>
+ {{/each}}
+ {{#notEmpty items}}
+ <li class='divider'></li>
+ {{/notEmpty}}
+ <li>
+ <a class='search-navigator-filters-manage' href='{{link '/issues/manage'}}'>{{t 'manage'}}</a>
+ </li>
+ </ul>
+ {{#if filter.description}}
+ <div class='search-navigator-filters-description'>{{filter.description}}</div>
+ {{/if}}
+ {{else}}
+ <span class='search-navigator-filters-name'>{{t 'issues'}}</span>
+ {{/if}}
+</h1>
+
+<div class='page-actions'>
+ <div class='button-group'>
+ {{#if state.canManageFilters}}
+ {{#if filter.canModify}}
+ {{#if state.changed}}
+ <button class='js-filter-save' id='issues-filter-save'>{{t 'save'}}</button>
+ {{/if}}
+ {{/if}}
+
+ {{#unless filter.id}}
+ <button class='js-filter-save-as' id='issues-filter-save-as'>{{t 'save_as'}}</button>
+ {{/unless}}
+
+ {{#if filter.id}}
+ {{#unless state.changed}}
+ <button class='js-filter-copy' id='issues-filter-copy'>{{t 'copy'}}</button>
+ {{/unless}}
+ {{/if}}
+
+ {{#if filter.canModify}}
+ {{#if filter.id}}
+ <button class='js-filter-edit' id='issues-filter-edit'>{{t 'edit'}}</button>
+ {{/if}}
+ {{/if}}
+ {{/if}}
+ </div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs
new file mode 100644
index 00000000000..1954f67f62d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter-form.hbs
@@ -0,0 +1,71 @@
+<h6>{{t 'issue.filter_similar_issues'}}</h6>
+
+<div class='issue-action-options'>
+ <a href='#' class='issue-action-option' data-property='severities' data-value='{{s}}'>
+ {{severityIcon severity}}&nbsp;{{t 'severity' severity}}
+ </a>
+
+ <a href='#' class='issue-action-option' data-property='statuses' data-value='{{status}}'>
+ {{statusIcon status}}&nbsp;{{t 'issue.status' status}}
+ </a>
+
+ {{#if resolution}}
+ <a href='#' class='issue-action-option' data-property='resolutions' data-value='{{resolution}}'>
+ {{t 'issue.resolution' resolution}}
+ </a>
+ {{else}}
+ <a href='#' class='issue-action-option' data-property='resolved' data-value='false'>
+ {{t 'unresolved'}}
+ </a>
+ {{/if}}
+
+ {{#if assignee}}
+ <a href='#' class='issue-action-option' data-property='assignees' data-value='{{assignee}}'>
+ {{t 'assigned_to'}} {{assigneeName}}
+ </a>
+ {{else}}
+ <a href='#' class='issue-action-option' data-property='assigned' data-value='false'>
+ {{t 'unassigned'}}
+ </a>
+ {{/if}}
+
+ {{#if actionPlan}}
+ <a href='#' class='issue-action-option' data-property='actionPlans' data-value='{{actionPlan}}'>
+ {{t 'issue.planned_for'}} {{actionPlanName}}
+ </a>
+ {{else}}
+ <a href='#' class='issue-action-option' data-property='planned' data-value='false'>
+ {{t 'issue.unplanned'}}
+ </a>
+ {{/if}}
+
+ <hr>
+
+ <a href='#' class='issue-action-option' data-property='rules' data-value='{{rule}}'>
+ {{limitString ruleName}}
+ </a>
+
+ {{#each tags}}
+ <a href='#' class='issue-action-option' data-property='tags' data-value='{{this}}'>
+ <i class='icon-tags icon-half-transparent'></i>&nbsp;{{this}}
+ </a>
+ {{/each}}
+
+ <hr>
+
+ <a href='#' class='issue-action-option' data-property='projectUuids' data-value='{{projectUuid}}'>
+ {{qualifierIcon 'TRK'}}&nbsp;{{projectLongName}}
+ </a>
+
+ {{#if subProject}}
+ <a href='#' class='issue-action-option' data-property='moduleUuids' data-value='{{subProjectUuid}}'>
+ {{qualifierIcon 'BRC'}}&nbsp;{{subProjectLongName}}
+ </a>
+ {{/if}}
+
+ <a href='#' class='issue-action-option' data-property='fileUuids' data-value='{{componentUuid}}'>
+ {{qualifierIcon componentQualifier}}&nbsp;{{fileFromPath componentLongName}}
+ </a>
+</div>
+
+<div class='bubble-popup-arrow'></div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs
new file mode 100644
index 00000000000..3ab307c3958
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-issue-filter.hbs
@@ -0,0 +1,5 @@
+<div class='issue-meta'>
+ <a class='issue-action issue-action-with-options js-issue-filter' href='#'>
+ <i class='icon-filter icon-half-transparent'></i>&nbsp;<i class='icon-dropdown'></i>
+ </a>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs
new file mode 100644
index 00000000000..cd434ab2340
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-layout.hbs
@@ -0,0 +1,11 @@
+<div class='search-navigator-side'>
+ <div class='search-navigator-filters'></div>
+ <div class='search-navigator-facets'></div>
+</div>
+
+<div class='search-navigator-workspace'>
+ <div class='search-navigator-workspace-header'></div>
+ <div class='search-navigator-workspace-list'></div>
+ <div class='issues-workspace-component-viewer'></div>
+ <div class='issues-workspace-home'></div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs
new file mode 100644
index 00000000000..2c7d2d6e3ee
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-header.hbs
@@ -0,0 +1,37 @@
+<div class='issues-header-component nowrap'>
+ {{#if state.component}}
+ <a class='js-back'>{{t 'issues.return_to_list'}}</a>&nbsp;&nbsp;&nbsp;
+
+ {{#with state.component}}
+ {{qualifierIcon 'TRK'}}&nbsp;<a href='{{dashboardUrl project}}' title='{{projectName}}'>{{projectName}}</a>
+ &nbsp;&nbsp;
+ {{qualifierIcon qualifier}}&nbsp;<a href='{{dashboardUrl key}}' title='{{name}}'>{{name}}</a>
+ {{/with}}
+ {{else}}
+ &nbsp;
+ {{/if}}
+</div>
+
+
+<div class='search-navigator-header-actions'>
+ {{#notNull state.total}}
+ <div class='search-navigator-header-pagination'>
+ {{#gt state.total 0}}
+ <a class='js-prev icon-prev' title='{{t 'paging_previous'}}'></a>
+ <span class='current'>{{sum state.selectedIndex 1}} / <span id='issues-total'>{{state.total}}</span></span>
+ <a class='js-next icon-next' title='{{t 'paging_next'}}'></a>
+ {{else}}
+ <span class='current'>0 / <span id='issues-total'>0</span></span>
+ {{/gt}}
+ </div>
+ {{/notNull}}
+
+
+ <div class='search-navigator-header-buttons button-group'>
+ <button id='issues-reload' class='js-reload'>{{t 'reload'}}</button>
+ <button class='js-new-search' id='issues-new-search'>{{t 'issue_filter.new_search'}}</button>
+ {{#if state.canBulkChange}}
+ <button id='issues-bulk-change' class='js-bulk-change'>{{t 'bulk_change'}}</button>
+ {{/if}}
+ </div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-home.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-home.hbs
new file mode 100644
index 00000000000..374dad390b7
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-home.hbs
@@ -0,0 +1,77 @@
+<div class='spacer-top spacer-bottom'>
+ <div class='columns'>
+ <div class='column-half {{#unless user}}column-one{{/unless}}'>
+ <h3 class='text-center'>{{t 'issues.home.recent_issues'}}</h3>
+ <p class='note text-center'>({{t 'issues.home.over_last_week'}})</p>
+
+ <div class='spacer-top text-center js-barchart' data-height='75' data-width='300'></div>
+ <h4 class='spacer-top spacer-bottom text-center'>{{t 'issues.home.projects'}}</h4>
+ <table class='data zebra spacer-top'>
+ {{#each projects}}
+ <tr>
+ <td>{{qualifierIcon 'TRK'}}&nbsp;<a href='{{issuesHomeLink 'projectUuids' val}}'>{{label}}</a></td>
+ <td class='thin text-right'>+{{numberShort count}}</td>
+ </tr>
+ {{/each}}
+ </table>
+ <h4 class='spacer-top spacer-bottom text-center'>{{t 'issues.home.authors'}}</h4>
+ <table class='data zebra spacer-top'>
+ {{#each authors}}
+ <tr>
+ <td><a href='{{issuesHomeLink 'authors' val}}'>{{val}}</a></td>
+ <td class='thin text-right'>+{{numberShort count}}</td>
+ </tr>
+ {{/each}}
+ </table>
+ <h4 class='spacer-top spacer-bottom text-center'>{{t 'issues.home.tags'}}</h4>
+ <ul class='list-inline'>
+ {{#each tags}}
+ <li><a class='link-no-underline' style='font-size: {{size}}px;' data-toggle='tooltip' data-placement='bottom'
+ href='{{issuesHomeLink 'tags' val}}'
+ title='+{{numberShort count}}'>{{val}}</a></li>
+ {{/each}}
+ </ul>
+ </div>
+
+ {{#if user}}
+ <div class='column-half'>
+ <h3 class='text-center'>{{t 'issues.home.my_recent_issues'}}</h3>
+ <p class='note text-center'>({{t 'issues.home.over_last_week'}})</p>
+
+ <div class='spacer-top text-center js-my-barchart' data-height='75' data-width='300'></div>
+ {{#notEmpty myProjects}}
+ <h4 class='spacer-top spacer-bottom text-center'>{{t 'issues.home.projects'}}</h4>
+ <table class='data zebra spacer-top'>
+ {{#each myProjects}}
+ <tr>
+ <td>{{qualifierIcon 'TRK'}}&nbsp;<a href='{{myIssuesHomeLink 'projectUuids' val}}'>{{label}}</a></td>
+ <td class='thin text-right'>+{{numberShort count}}</td>
+ </tr>
+ {{/each}}
+ </table>
+ {{/notEmpty}}
+ {{#notEmpty myTags}}
+ <h4 class='spacer-top spacer-bottom text-center'>{{t 'issues.home.tags'}}</h4>
+ <ul class='list-inline'>
+ {{#each myTags}}
+ <li><a class='link-no-underline' style='font-size: {{size}}px;' data-toggle='tooltip' data-placement='bottom'
+ href='{{myIssuesHomeLink 'tags' val}}'
+ title='+{{numberShort count}}'>{{val}}</a></li>
+ {{/each}}
+ </ul>
+ {{/notEmpty}}
+ {{#notEmpty filters}}
+ <h3 class='spacer-bottom text-center' style='padding-top: 12px; margin-top: 20px; border-top: 1px solid #efefef;'>
+ {{t 'issues.home.my_filters'}}</h3>
+ <ul class='list-inline'>
+ {{#each filters}}
+ <li>
+ <a href='{{issueFilterHomeLink id}}'>{{name}}</a>
+ </li>
+ {{/each}}
+ </ul>
+ {{/notEmpty}}
+ </div>
+ {{/if}}
+ </div>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs
new file mode 100644
index 00000000000..73d15ac8fbe
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list-component.hbs
@@ -0,0 +1,13 @@
+<div class='issues-workspace-list-component'>
+ <a class='issues-workspace-list-component-part' href='{{dashboardUrl project}}'>
+ {{qualifierIcon 'TRK'}}&nbsp;{{projectLongName}}
+ </a>
+ {{#if subProject}}
+ <a class='issues-workspace-list-component-part' href='{{dashboardUrl subProject}}'>
+ {{qualifierIcon 'TRK'}}&nbsp;{{subProjectLongName}}
+ </a>
+ {{/if}}
+ <a class='issues-workspace-list-component-part' href='{{dashboardUrl component}}'>
+ {{qualifierIcon componentQualifier}}&nbsp;{{componentLongName}}
+ </a>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs
new file mode 100644
index 00000000000..a81b673736c
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/templates/issues-workspace-list.hbs
@@ -0,0 +1,5 @@
+<div class='js-list'></div>
+
+<div class='search-navigator-workspace-list-more js-more'>
+ <i class='spinner'></i>
+</div>
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
new file mode 100644
index 00000000000..620b68bfccd
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/workspace-header-view.js
@@ -0,0 +1,47 @@
+define([
+ 'components/navigator/workspace-header-view',
+ './templates'
+], function (WorkspaceHeaderView) {
+
+ var $ = jQuery;
+
+ return WorkspaceHeaderView.extend({
+ template: Templates['issues-workspace-header'],
+
+ events: function () {
+ return _.extend(WorkspaceHeaderView.prototype.events.apply(this, arguments), {
+ 'click .js-back': 'returnToList',
+ 'click .js-new-search': 'newSearch'
+ });
+ },
+
+ initialize: function () {
+ var that = this;
+ WorkspaceHeaderView.prototype.initialize.apply(this, arguments);
+ this._onBulkIssues = window.onBulkIssues;
+ window.onBulkIssues = function () {
+ $('#modal').dialog('close');
+ return that.options.app.controller.fetchList();
+ };
+ },
+
+ onClose: function () {
+ window.onBulkIssues = this._onBulkIssues;
+ },
+
+ returnToList: function () {
+ this.options.app.controller.closeComponentViewer();
+ },
+
+ newSearch: function () {
+ this.options.app.controller.newSearch();
+ },
+
+ bulkChange: function () {
+ var query = this.options.app.controller.getQuery('&', true),
+ url = baseUrl + '/issues/bulk_change_form?' + query;
+ window.openModalWindow(url, {});
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-home-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-home-view.js
new file mode 100644
index 00000000000..408900fb63e
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/workspace-home-view.js
@@ -0,0 +1,160 @@
+define([
+ 'widgets/issue-filter/widget',
+ './templates'
+], function (IssueFilter) {
+
+ var $ = jQuery;
+
+ Handlebars.registerHelper('issuesHomeLink', function (property, value) {
+ return baseUrl + '/issues/search#resolved=false|createdInLast=1w|' +
+ property + '=' + (encodeURIComponent(value));
+ });
+
+ Handlebars.registerHelper('myIssuesHomeLink', function (property, value) {
+ return baseUrl + '/issues/search#resolved=false|createdInLast=1w|assignees=__me__|' +
+ property + '=' + (encodeURIComponent(value));
+ });
+
+ Handlebars.registerHelper('issueFilterHomeLink', function (id) {
+ return baseUrl + '/issues/search#id=' + id;
+ });
+
+ return Marionette.ItemView.extend({
+ template: Templates['issues-workspace-home'],
+
+ modelEvents: {
+ 'change': 'render'
+ },
+
+ events: {
+ 'click .js-barchart rect': 'selectBar',
+ 'click .js-my-barchart rect': 'selectMyBar'
+ },
+
+ initialize: function () {
+ this.model = new Backbone.Model();
+ this.requestIssues();
+ this.requestMyIssues();
+ },
+
+ _getProjects: function (r) {
+ var projectFacet = _.findWhere(r.facets, { property: 'projectUuids' });
+ if (projectFacet != null) {
+ var values = _.head(projectFacet.values, 3);
+ values.forEach(function (v) {
+ var project = _.findWhere(r.projects, { uuid: v.val });
+ v.label = project.longName;
+ });
+ return values;
+ }
+ },
+
+ _getAuthors: function (r) {
+ var authorFacet = _.findWhere(r.facets, { property: 'authors' });
+ if (authorFacet != null) {
+ return _.head(authorFacet.values, 3);
+ }
+ },
+
+ _getTags: function (r) {
+ var MIN_SIZE = 10,
+ MAX_SIZE = 24,
+ tagFacet = _.findWhere(r.facets, { property: 'tags' });
+ if (tagFacet != null) {
+ var values = _.head(tagFacet.values, 10),
+ minCount = _.min(values, function (v) {
+ return v.count;
+ }).count,
+ maxCount = _.max(values, function (v) {
+ return v.count;
+ }).count,
+ scale = d3.scale.linear().domain([minCount, maxCount]).range([MIN_SIZE, MAX_SIZE]);
+ values.forEach(function (v) {
+ v.size = scale(v.count);
+ });
+ return values;
+ }
+ },
+
+ requestIssues: function () {
+ var that = this;
+ var url = baseUrl + '/api/issues/search',
+ options = {
+ resolved: false,
+ createdInLast: '1w',
+ ps: 1,
+ facets: 'createdAt,projectUuids,authors,tags'
+ };
+ return $.get(url, options).done(function (r) {
+ var createdAt = _.findWhere(r.facets, { property: 'createdAt' });
+ that.model.set({
+ createdAt: createdAt != null ? createdAt.values : null,
+ projects: that._getProjects(r),
+ authors: that._getAuthors(r),
+ tags: that._getTags(r)
+ });
+ });
+ },
+
+ requestMyIssues: function () {
+ var that = this;
+ var url = baseUrl + '/api/issues/search',
+ options = {
+ resolved: false,
+ createdInLast: '1w',
+ assignees: '__me__',
+ ps: 1,
+ facets: 'createdAt,projectUuids,authors,tags'
+ };
+ return $.get(url, options).done(function (r) {
+ var createdAt = _.findWhere(r.facets, { property: 'createdAt' });
+ return that.model.set({
+ myCreatedAt: createdAt != null ? createdAt.values : null,
+ myProjects: that._getProjects(r),
+ myTags: that._getTags(r)
+ });
+ });
+ },
+
+ onRender: function () {
+ var values = this.model.get('createdAt'),
+ myValues = this.model.get('myCreatedAt');
+ if (values != null) {
+ this.$('.js-barchart').barchart(values);
+ }
+ if (myValues != null) {
+ this.$('.js-my-barchart').barchart(myValues);
+ }
+ this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
+ },
+
+ selectBar: function (e) {
+ var periodStart = $(e.currentTarget).data('period-start'),
+ periodEnd = $(e.currentTarget).data('period-end');
+ this.options.app.state.setQuery({
+ resolved: false,
+ createdAfter: periodStart,
+ createdBefore: periodEnd
+ });
+ },
+
+ selectMyBar: function (e) {
+ var periodStart = $(e.currentTarget).data('period-start'),
+ periodEnd = $(e.currentTarget).data('period-end');
+ this.options.app.state.setQuery({
+ resolved: false,
+ assignees: '__me__',
+ createdAfter: periodStart,
+ createdBefore: periodEnd
+ });
+ },
+
+ serializeData: function () {
+ return _.extend(Marionette.ItemView.prototype.serializeData.apply(this, arguments), {
+ user: window.SS.user,
+ filters: _.sortBy(this.options.app.filters.toJSON(), 'name')
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js
new file mode 100644
index 00000000000..ada57f15a2d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/workspace-list-empty-view.js
@@ -0,0 +1,11 @@
+define(function () {
+
+ return Marionette.ItemView.extend({
+ className: 'search-navigator-no-results',
+
+ template: function () {
+ return t('issue_filter.no_issues');
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js
new file mode 100644
index 00000000000..6d3df9c6901
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/workspace-list-item-view.js
@@ -0,0 +1,113 @@
+define([
+ 'components/issue/issue-view',
+ './issue-filter-view',
+ './templates'
+], function (IssueView, IssueFilterView) {
+
+ var $ = jQuery,
+ SHOULD_NULL = {
+ any: ['issues'],
+ resolutions: ['resolved'],
+ resolved: ['resolutions'],
+ assignees: ['assigned'],
+ assigned: ['assignees'],
+ actionPlans: ['planned'],
+ planned: ['actionPlans']
+ };
+
+ return IssueView.extend({
+ filterTemplate: Templates['issues-issue-filter'],
+
+ events: function () {
+ return _.extend(IssueView.prototype.events.apply(this, arguments), {
+ 'click': 'selectCurrent',
+ 'dblclick': 'openComponentViewer',
+ 'click .js-issue-navigate': 'openComponentViewer',
+ 'click .js-issue-filter': 'onIssueFilterClick'
+ });
+ },
+
+ initialize: function (options) {
+ IssueView.prototype.initialize.apply(this, arguments);
+ this.listenTo(options.app.state, 'change:selectedIndex', this.select);
+ },
+
+ onRender: function () {
+ IssueView.prototype.onRender.apply(this, arguments);
+ this.select();
+ this.addFilterSelect();
+ this.$el.addClass('issue-navigate-right');
+ },
+
+ onIssueFilterClick: function (e) {
+ var that = this;
+ e.preventDefault();
+ e.stopPropagation();
+ $('body').click();
+ this.popup = new IssueFilterView({
+ triggerEl: $(e.currentTarget),
+ bottomRight: true,
+ model: this.model
+ });
+ this.popup.on('select', function (property, value) {
+ var obj;
+ obj = {};
+ obj[property] = '' + value;
+ SHOULD_NULL.any.forEach(function (p) {
+ obj[p] = null;
+ });
+ if (SHOULD_NULL[property] != null) {
+ SHOULD_NULL[property].forEach(function (p) {
+ obj[p] = null;
+ });
+ }
+ that.options.app.state.updateFilter(obj);
+ that.popup.close();
+ });
+ this.popup.render();
+ },
+
+ addFilterSelect: function () {
+ this.$('.issue-table-meta-cell-first')
+ .find('.issue-meta-list')
+ .append(this.filterTemplate(this.model.toJSON()));
+ },
+
+ select: function () {
+ var selected = this.model.get('index') === this.options.app.state.get('selectedIndex');
+ this.$el.toggleClass('selected', selected);
+ },
+
+ selectCurrent: function () {
+ this.options.app.state.set({ selectedIndex: this.model.get('index') });
+ },
+
+ resetIssue: function (options) {
+ var that = this;
+ var key = this.model.get('key'),
+ componentUuid = this.model.get('componentUuid'),
+ index = this.model.get('index');
+ this.model.clear({ silent: true });
+ this.model.set({ key: key, componentUuid: componentUuid, index: index }, { silent: true });
+ return this.model.fetch(options).done(function () {
+ return that.trigger('reset');
+ });
+ },
+
+ openComponentViewer: function () {
+ this.options.app.state.set({ selectedIndex: this.model.get('index') });
+ if (this.options.app.state.has('component')) {
+ return this.options.app.controller.closeComponentViewer();
+ } else {
+ return this.options.app.controller.showComponentViewer(this.model);
+ }
+ },
+
+ serializeData: function () {
+ return _.extend(IssueView.prototype.serializeData.apply(this, arguments), {
+ showComponent: true
+ });
+ }
+ });
+
+});
diff --git a/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js
new file mode 100644
index 00000000000..37e7ade70e9
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/issues/workspace-list-view.js
@@ -0,0 +1,106 @@
+define([
+ 'components/navigator/workspace-list-view',
+ './workspace-list-item-view',
+ './workspace-list-empty-view',
+ './templates'
+], function (WorkspaceListView, IssueView, EmptyView) {
+
+ var $ = jQuery,
+ COMPONENT_HEIGHT = 29,
+ BOTTOM_OFFSET = 10;
+
+ return WorkspaceListView.extend({
+ template: Templates['issues-workspace-list'],
+ componentTemplate: Templates['issues-workspace-list-component'],
+ itemView: IssueView,
+ itemViewContainer: '.js-list',
+ emptyView: EmptyView,
+
+ bindShortcuts: function () {
+ var that = this;
+ var doAction = function (action) {
+ var selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex'));
+ if (selectedIssue == null) {
+ return;
+ }
+ var selectedIssueView = that.children.findByModel(selectedIssue);
+ selectedIssueView.$('.js-issue-' + action).click();
+ };
+ WorkspaceListView.prototype.bindShortcuts.apply(this, arguments);
+ key('right', 'list', function () {
+ var selectedIssue = that.collection.at(that.options.app.state.get('selectedIndex'));
+ that.options.app.controller.showComponentViewer(selectedIssue);
+ return false;
+ });
+ key('f', 'list', function () {
+ return doAction('transition');
+ });
+ key('a', 'list', function () {
+ return doAction('assign');
+ });
+ key('m', 'list', function () {
+ return doAction('assign-to-me');
+ });
+ key('p', 'list', function () {
+ return doAction('plan');
+ });
+ key('i', 'list', function () {
+ return doAction('set-severity');
+ });
+ key('c', 'list', function () {
+ return doAction('comment');
+ });
+ return key('t', 'list', function () {
+ return doAction('edit-tags');
+ });
+ },
+
+ scrollTo: function () {
+ var selectedIssue = this.collection.at(this.options.app.state.get('selectedIndex'));
+ if (selectedIssue == null) {
+ return;
+ }
+ var selectedIssueView = this.children.findByModel(selectedIssue),
+ parentTopOffset = this.$el.offset().top,
+ viewTop = selectedIssueView.$el.offset().top - parentTopOffset;
+ if (selectedIssueView.$el.prev().is('.issues-workspace-list-component')) {
+ viewTop -= COMPONENT_HEIGHT;
+ }
+ var viewBottom = selectedIssueView.$el.offset().top + selectedIssueView.$el.outerHeight() + BOTTOM_OFFSET,
+ windowTop = $(window).scrollTop(),
+ windowBottom = windowTop + $(window).height();
+ if (viewTop < windowTop) {
+ $(window).scrollTop(viewTop);
+ }
+ if (viewBottom > windowBottom) {
+ $(window).scrollTop($(window).scrollTop() - windowBottom + viewBottom);
+ }
+ },
+
+ appendHtml: function (compositeView, itemView, index) {
+ var $container = this.getItemViewContainer(compositeView),
+ model = this.collection.at(index);
+ if (model != null) {
+ var prev = this.collection.at(index - 1),
+ putComponent = prev == null;
+ if (prev != null) {
+ var fullComponent = [model.get('project'), model.get('component')].join(' '),
+ fullPrevComponent = [prev.get('project'), prev.get('component')].join(' ');
+ if (fullComponent !== fullPrevComponent) {
+ putComponent = true;
+ }
+ }
+ if (putComponent) {
+ $container.append(this.componentTemplate(model.toJSON()));
+ }
+ }
+ $container.append(itemView.el);
+ },
+
+ closeChildren: function () {
+ WorkspaceListView.prototype.closeChildren.apply(this, arguments);
+ this.$('.issues-workspace-list-component').remove();
+ }
+ });
+
+});