From 57729e6625cbd1bd0b0ce35e54966481e2a1c88b Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 22 Jan 2015 14:45:38 +0100 Subject: [PATCH] SONAR-5966 improve display of issue boxes --- .../src/main/coffee/issue/issue-view.coffee | 39 +++-- .../issue/views/assign-form-view.coffee | 6 +- .../issue/views/set-severity-form-view.coffee | 6 +- .../coffee/issue/views/tags-form-view.coffee | 145 ++++++++++++++++++ .../issue/views/transitions-form-view.coffee | 36 +++++ .../issues/component-viewer/main.coffee | 12 +- .../coffee/issues/workspace-list-view.coffee | 13 +- .../main/hbs/issue/issue-tags-form-option.hbs | 15 ++ .../src/main/hbs/issue/issue-tags-form.hbs | 6 + .../main/hbs/issue/issue-transitions-form.hbs | 9 ++ server/sonar-web/src/main/hbs/issue/issue.hbs | 48 +++--- .../src/main/hbs/issues/issues-help.hbs | 5 +- .../js/tests/e2e/tests/issues-page-spec.js | 93 ++++------- .../src/main/less/components/issues.less | 28 +--- 14 files changed, 302 insertions(+), 159 deletions(-) create mode 100644 server/sonar-web/src/main/coffee/issue/views/tags-form-view.coffee create mode 100644 server/sonar-web/src/main/coffee/issue/views/transitions-form-view.coffee create mode 100644 server/sonar-web/src/main/hbs/issue/issue-tags-form-option.hbs create mode 100644 server/sonar-web/src/main/hbs/issue/issue-tags-form.hbs create mode 100644 server/sonar-web/src/main/hbs/issue/issue-transitions-form.hbs diff --git a/server/sonar-web/src/main/coffee/issue/issue-view.coffee b/server/sonar-web/src/main/coffee/issue/issue-view.coffee index 669179a6648..a8e09a3ca4d 100644 --- a/server/sonar-web/src/main/coffee/issue/issue-view.coffee +++ b/server/sonar-web/src/main/coffee/issue/issue-view.coffee @@ -6,12 +6,14 @@ define [ 'issue/views/issue-popup' + 'issue/views/transitions-form-view' 'issue/views/assign-form-view' 'issue/views/comment-form-view' 'issue/views/plan-form-view' 'issue/views/set-severity-form-view' 'issue/views/more-actions-view' 'issue/views/rule-overlay' + 'issue/views/tags-form-view' 'templates/issue' @@ -23,12 +25,14 @@ define [ IssuePopup + TransitionsFormView AssignFormView CommentFormView PlanFormView SetSeverityFormView MoreActionsView RuleOverlay + TagsFormView ) -> @@ -66,9 +70,7 @@ define [ 'click .js-issue-show-changelog': 'showChangeLog' 'click .js-issue-more': 'showMoreActions' 'click .js-issue-rule': 'showRule' - 'click @ui.tagsChange': 'changeTags' - 'click @ui.tagsEditDone': 'editDone' - 'click @ui.tagsEditCancel': 'cancelEdit' + 'click .js-issue-edit-tags': 'editTags' onRender: -> @@ -172,22 +174,19 @@ define [ .done => @updateAfterAction true window.process.finishBackgroundProcess p - .fail (r) => + .fail => window.process.failBackgroundProcess p transition: (e) -> - p = window.process.addBackgroundProcess() - $.ajax - type: 'POST', - url: baseUrl + '/api/issues/do_transition', - data: - issue: @model.get('key') - transition: $(e.currentTarget).data 'transition' - .done => - @resetIssue {}, p - .fail => - window.process.failBackgroundProcess p + e.stopPropagation() + $('body').click() + @popup = new TransitionsFormView + triggerEl: $(e.currentTarget) + bottom: true + model: @model + view: @ + @popup.render() setSeverity: (e) -> @@ -267,6 +266,16 @@ define [ ruleOverlay.render() + editTags: (e)-> + e.stopPropagation() + $('body').click() + @popup = new TagsFormView + triggerEl: $(e.currentTarget) + bottomRight: true + model: @model + @popup.render() + + changeTags: -> p = window.process.addBackgroundProcess() jQuery.ajax diff --git a/server/sonar-web/src/main/coffee/issue/views/assign-form-view.coffee b/server/sonar-web/src/main/coffee/issue/views/assign-form-view.coffee index ebff3de408a..bca4eb2d641 100644 --- a/server/sonar-web/src/main/coffee/issue/views/assign-form-view.coffee +++ b/server/sonar-web/src/main/coffee/issue/views/assign-form-view.coffee @@ -36,11 +36,11 @@ define [ onRender: -> super - @renderAssignees() + @renderTags() setTimeout (=> @$('input').focus()), 100 - renderAssignees: -> + renderTags: -> @$('.issue-action-option').remove() @getAssignees().forEach @renderAssignee, @ @selectInitialOption() @@ -118,7 +118,7 @@ define [ @assignees = users.map (user) -> id: user.login text: user.name - @renderAssignees() + @renderTags() getAssignees: -> diff --git a/server/sonar-web/src/main/coffee/issue/views/set-severity-form-view.coffee b/server/sonar-web/src/main/coffee/issue/views/set-severity-form-view.coffee index b8110244861..836ef3babaa 100644 --- a/server/sonar-web/src/main/coffee/issue/views/set-severity-form-view.coffee +++ b/server/sonar-web/src/main/coffee/issue/views/set-severity-form-view.coffee @@ -12,12 +12,12 @@ define [ template: Templates['issue-set-severity-form'] - getSeverity: -> + getTransition: -> @model.get 'severity' selectInitialOption: -> - @makeActive @getOptions().filter("[data-value=#{@getSeverity()}]") + @makeActive @getOptions().filter("[data-value=#{@getTransition()}]") selectOption: (e) -> @@ -27,7 +27,7 @@ define [ submit: (severity) -> - _severity = @getSeverity() + _severity = @getTransition() return if severity == _severity p = window.process.addBackgroundProcess() @model.set severity: severity diff --git a/server/sonar-web/src/main/coffee/issue/views/tags-form-view.coffee b/server/sonar-web/src/main/coffee/issue/views/tags-form-view.coffee new file mode 100644 index 00000000000..6b32590a270 --- /dev/null +++ b/server/sonar-web/src/main/coffee/issue/views/tags-form-view.coffee @@ -0,0 +1,145 @@ +define [ + 'issue/views/action-options-view' + 'templates/issue' +], ( + ActionOptionsView +) -> + + $ = jQuery + + + class extends ActionOptionsView + template: Templates['issue-tags-form'] + optionTemplate: Templates['issue-tags-form-option'] + + + modelEvents: + 'change:tags': 'renderTags' + + + events: -> + _.extend super, + 'click input': 'onInputClick' + 'keydown input': 'onInputKeydown' + 'keyup input': 'onInputKeyup' + + + initialize: -> + super + @query = '' + @tags = [] + @selected = 0 + @debouncedSearch = _.debounce @search, 250 + @requestTags() + + + requestTags: -> + $.get "#{baseUrl}/api/issues/tags", ps: 0 + .done (data) => + @tags = data.tags + @renderTags() + + + onRender: -> + super + @renderTags() + setTimeout (=> @$('input').focus()), 100 + + + selectInitialOption: -> + @selected = Math.max Math.min(@selected, @getOptions().length - 1), 0 + @makeActive @getOptions().eq @selected + + + filterTags: (tags) -> + _.filter tags, (tag) => tag.indexOf(@query) != -1 + + + renderTags: -> + @$('.issue-action-option').remove() + @filterTags(@getTags()).forEach @renderSelectedTag, @ + @filterTags(_.difference(@tags, @getTags())).forEach @renderTag, @ + if @query.length > 0 && @tags.indexOf(@query) == -1 && @getTags().indexOf(@query) == -1 + @renderCustomTag @query + @selectInitialOption() + + + renderSelectedTag: (tag) -> + html = @optionTemplate { tag: tag, selected: true, custom: false } + @$('.issue-action-options').append html + + + renderTag: (tag) -> + html = @optionTemplate { tag: tag, selected: false, custom: false } + @$('.issue-action-options').append html + + + renderCustomTag: (tag) -> + html = @optionTemplate { tag: tag, selected: false, custom: true } + @$('.issue-action-options').append html + + + selectOption: (e) -> + e.preventDefault() + e.stopPropagation() + tags = @getTags().slice() + tag = $(e.currentTarget).data 'value' + if $(e.currentTarget).data('selected')? + tags = _.without tags, tag + else + tags.push tag + @selected = @getOptions().index $(e.currentTarget) + @submit tags + + + submit: (tags) -> + _tags = @getTags() + @model.set tags: tags + p = window.process.addBackgroundProcess() + $.ajax + type: 'POST' + url: "#{baseUrl}/api/issues/set_tags" + data: + key: @model.id + tags: tags.join() + .done => + window.process.finishBackgroundProcess p + .fail => + @model.set tags: _tags + window.process.failBackgroundProcess p + + + onInputClick: (e) -> + e.stopPropagation() + + + onInputKeydown: (e) -> + @query = @$('input').val() + return @selectPreviousOption() if e.keyCode == 38 # up + return @selectNextOption() if e.keyCode == 40 # down + return @selectActiveOption() if e.keyCode == 13 # return + return false if e.keyCode == 9 # tab + @close() if e.keyCode == 27 # escape + + + onInputKeyup: -> + query = @$('input').val() + if query != @query + @query = query + @debouncedSearch query + + + search: (query) -> + @query = query + @renderTags() + + + resetAssignees: (users) -> + @assignees = users.map (user) -> + id: user.login + text: user.name + @renderTags() + + + getTags: -> + @model.get('tags') || [] diff --git a/server/sonar-web/src/main/coffee/issue/views/transitions-form-view.coffee b/server/sonar-web/src/main/coffee/issue/views/transitions-form-view.coffee new file mode 100644 index 00000000000..f389cbf3c6d --- /dev/null +++ b/server/sonar-web/src/main/coffee/issue/views/transitions-form-view.coffee @@ -0,0 +1,36 @@ +define [ + 'issue/views/action-options-view' + 'templates/issue' +], ( + ActionOptionsView +) -> + + $ = jQuery + + + class extends ActionOptionsView + template: Templates['issue-transitions-form'] + + + selectInitialOption: -> + @makeActive @getOptions().first() + + + selectOption: (e) -> + transition = $(e.currentTarget).data 'value' + @submit transition + super + + + submit: (transition) -> + p = window.process.addBackgroundProcess() + $.ajax + type: 'POST', + url: baseUrl + '/api/issues/do_transition', + data: + issue: @model.get('key') + transition: transition + .done => + @options.view.resetIssue {}, p + .fail => + window.process.failBackgroundProcess p diff --git a/server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee b/server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee index 9979b1ad5ba..09b1dc11b18 100644 --- a/server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee +++ b/server/sonar-web/src/main/coffee/issues/component-viewer/main.coffee @@ -36,11 +36,6 @@ define [ bindShortcuts: -> - doTransition = (transition) => - selectedIssueView = @getSelectedIssueEl() - return unless selectedIssueView - selectedIssueView.find("[data-transition=#{transition}]").click() - doAction = (action) => selectedIssueView = @getSelectedIssueEl() return unless selectedIssueView @@ -58,12 +53,7 @@ define [ @options.app.controller.closeComponentViewer() false - key 'c', 'componentViewer', -> doTransition 'confirm' - key 'c', 'componentViewer', -> doTransition 'unconfirm' - key 'u', 'componentViewer', -> doTransition 'mute' - key 'r', 'componentViewer', -> doTransition 'resolve' - key 'r', 'componentViewer', -> doTransition 'reopen' - key 'f', 'componentViewer', -> doTransition 'falsepositive' + key 'f', 'componentViewer', -> doAction 'transition' key 'a', 'componentViewer', -> doAction 'assign' key 'm', 'componentViewer', -> doAction 'assign-to-me' key 'p', 'componentViewer', -> doAction 'plan' diff --git a/server/sonar-web/src/main/coffee/issues/workspace-list-view.coffee b/server/sonar-web/src/main/coffee/issues/workspace-list-view.coffee index b659fcf1b6f..ebe82875367 100644 --- a/server/sonar-web/src/main/coffee/issues/workspace-list-view.coffee +++ b/server/sonar-web/src/main/coffee/issues/workspace-list-view.coffee @@ -25,12 +25,6 @@ define [ bindShortcuts: -> - doTransition = (transition) => - selectedIssue = @collection.at @options.app.state.get 'selectedIndex' - return unless selectedIssue? - selectedIssueView = @children.findByModel selectedIssue - selectedIssueView.$("[data-transition=#{transition}]").click() - doAction = (action) => selectedIssue = @collection.at @options.app.state.get 'selectedIndex' return unless selectedIssue? @@ -44,12 +38,7 @@ define [ @options.app.controller.showComponentViewer selectedIssue return false - key 'c', 'list', -> doTransition 'confirm' - key 'c', 'list', -> doTransition 'unconfirm' - key 'u', 'list', -> doTransition 'mute' - key 'r', 'list', -> doTransition 'resolve' - key 'r', 'list', -> doTransition 'reopen' - key 'f', 'list', -> doTransition 'falsepositive' + key 'f', 'list', -> doAction 'transition' key 'a', 'list', -> doAction 'assign' key 'm', 'list', -> doAction 'assign-to-me' key 'p', 'list', -> doAction 'plan' diff --git a/server/sonar-web/src/main/hbs/issue/issue-tags-form-option.hbs b/server/sonar-web/src/main/hbs/issue/issue-tags-form-option.hbs new file mode 100644 index 00000000000..df054fe2466 --- /dev/null +++ b/server/sonar-web/src/main/hbs/issue/issue-tags-form-option.hbs @@ -0,0 +1,15 @@ + + + {{#if selected}} + + {{else}} + + {{/if}} + + {{#if custom}} + + {{tag}} + {{else}} + {{tag}} + {{/if}} + diff --git a/server/sonar-web/src/main/hbs/issue/issue-tags-form.hbs b/server/sonar-web/src/main/hbs/issue/issue-tags-form.hbs new file mode 100644 index 00000000000..9d20be8d079 --- /dev/null +++ b/server/sonar-web/src/main/hbs/issue/issue-tags-form.hbs @@ -0,0 +1,6 @@ +
+ + +
+ +
diff --git a/server/sonar-web/src/main/hbs/issue/issue-transitions-form.hbs b/server/sonar-web/src/main/hbs/issue/issue-transitions-form.hbs new file mode 100644 index 00000000000..ab64424207e --- /dev/null +++ b/server/sonar-web/src/main/hbs/issue/issue-transitions-form.hbs @@ -0,0 +1,9 @@ +
+ {{#each transitions}} + + {{t 'issue.transition' this}} + + {{/each}} +
+ +
diff --git a/server/sonar-web/src/main/hbs/issue/issue.hbs b/server/sonar-web/src/main/hbs/issue/issue.hbs index bf36dc75dc8..0c23a0576c4 100644 --- a/server/sonar-web/src/main/hbs/issue/issue.hbs +++ b/server/sonar-web/src/main/hbs/issue/issue.hbs @@ -2,14 +2,13 @@
{{message}}
+
+ {{ruleName}} +
-
- Rule -
- {{#if debt}}
{{t 'issue.debt'}} {{debt}} @@ -51,19 +50,16 @@
- {{statusHelper status resolution}} + {{#notEmpty transitions}} + + {{statusHelper status resolution}}  + + {{else}} + {{statusHelper status resolution}} + {{/notEmpty}}
- {{#notEmpty transitions}} -
- {{#each transitions}} - - {{t 'issue.transition' this}} - - {{/each}} -
- {{/notEmpty}} -
{{#inArray actions "assign"}} @@ -97,7 +93,7 @@ {{#inArray actions "comment"}}
{{t 'issue.comment.formlink' }} + class="issue-meta-label">
{{/inArray}} @@ -112,18 +108,16 @@ -
- - - {{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}} - +
{{#inArray actions "set_tags"}} - - - - - - {{t 'cancel'}} + + +  {{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}} +   + + {{else}} + +  {{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}} {{/inArray}}
diff --git a/server/sonar-web/src/main/hbs/issues/issues-help.hbs b/server/sonar-web/src/main/hbs/issues/issues-help.hbs index fe36051338e..1fc3cdcc65c 100644 --- a/server/sonar-web/src/main/hbs/issues/issues-help.hbs +++ b/server/sonar-web/src/main/hbs/issues/issues-help.hbs @@ -12,10 +12,7 @@

To control selected issue

    -
  • c    to confirm/unconfirm
  • -
  • r    to resolve/reopen
  • -
  • f    to mark as false positive
  • -
  • u    to mute
  • +
  • f    to do a transition
  • a    to assign
  • m    to assign to the current user
  • p    to plan
  • diff --git a/server/sonar-web/src/main/js/tests/e2e/tests/issues-page-spec.js b/server/sonar-web/src/main/js/tests/e2e/tests/issues-page-spec.js index 710a3068462..1fc62c032d6 100644 --- a/server/sonar-web/src/main/js/tests/e2e/tests/issues-page-spec.js +++ b/server/sonar-web/src/main/js/tests/e2e/tests/issues-page-spec.js @@ -79,15 +79,13 @@ casper.test.begin(testName('Issue Box', 'Check Elements'), function (test) { .then(function () { test.assertSelectorContains('.issue.selected', "Add a 'package-info.java' file to document the"); - test.assertExists('.issue.selected .issue-tags'); - test.assertSelectorContains('.issue.selected .issue-tags', 'issue.no_tag'); + test.assertExists('.issue.selected .js-issue-tags'); + test.assertSelectorContains('.issue.selected .js-issue-tags', 'issue.no_tag'); test.assertExists('.issue.selected .js-issue-set-severity'); test.assertSelectorContains('.issue.selected .js-issue-set-severity', 'MAJOR'); test.assertSelectorContains('.issue.selected', 'CONFIRMED'); - test.assertElementCount('.issue.selected .js-issue-transition', 3); - test.assertExists('.issue.selected [data-transition=unconfirm]'); - test.assertExists('.issue.selected [data-transition=resolve]'); - test.assertExists('.issue.selected [data-transition=falsepositive]'); + test.assertElementCount('.issue.selected .js-issue-transition', 1); + test.assertExists('.issue.selected .js-issue-transition'); test.assertExists('.issue.selected .js-issue-assign'); test.assertSelectorContains('.issue.selected .js-issue-assign', 'unassigned'); test.assertExists('.issue.selected .js-issue-plan'); @@ -111,25 +109,31 @@ casper.test.begin(testName('Issue Box', 'Tags'), function (test) { lib.mockRequest('/api/l10n/index', '{}'); lib.mockRequestFromFile('/api/issue_filters/app', 'app.json'); lib.mockRequestFromFile('/api/issues/search', 'search-with-tags.json'); - this.showMock = lib.mockRequestFromFile('/api/issues/show*', 'show-with-tags.json'); + lib.mockRequestFromFile('/api/issues/tags', 'tags.json'); + lib.mockRequestFromFile('/api/issues/set_tags', 'tags-modified.json'); }) .then(function () { - casper.waitForSelector('.issue.selected .issue-tags', function () { - test.assertSelectorContains('.issue.selected .issue-tags', 'security, cwe'); - lib.mockRequestFromFile('/api/issues/tags*', 'tags.json'); - casper.click('.issue.selected .issue-tag-list'); - - casper.waitForSelector('.issue.selected .select2-input', function () { - lib.mockRequestFromFile('/api/issues/set_tags', 'tags-modified.json'); - casper.click('.issue.selected .issue-tag-edit-done'); - casper.waitWhileVisible('.issue.selected .issue-tag-edit'); - casper.waitUntilVisible('.issue.selected .issue-tag-list', function () { - // TODO Find a way to have this assertion work - // test.assertSelectorContains('.issue.selected .issue-tags .issue-tag-list', 'security, cwe, cert'); - }); - }); - }); + casper.waitForSelector('.issue.selected .js-issue-tags'); + }) + + .then(function () { + test.assertSelectorContains('.issue.selected .js-issue-tags', 'security, cwe'); + casper.click('.issue.selected .js-issue-edit-tags'); + }) + + .then(function () { + casper.waitForSelector('.issue-action-option[data-value=design]'); + }) + + .then(function () { + casper.click('.issue-action-option[data-value=design]'); + test.assertSelectorContains('.issue.selected .js-issue-tags', 'security, cwe, design'); + }) + + .then(function () { + casper.click('.issue-action-option[data-value=cwe]'); + test.assertSelectorContains('.issue.selected .js-issue-tags', 'security, design'); }) .run(function () { @@ -146,54 +150,23 @@ casper.test.begin(testName('Issue Box', 'Transitions'), function (test) { lib.mockRequest('/api/l10n/index', '{}'); lib.mockRequestFromFile('/api/issue_filters/app', 'app.json'); lib.mockRequestFromFile('/api/issues/search', 'search.json'); - this.showMock = lib.mockRequestFromFile('/api/issues/show*', 'show.json'); + lib.mockRequestFromFile('/api/issues/show*', 'show.json'); lib.mockRequest('/api/issues/do_transition', '{}'); }) .then(function () { - casper.waitForSelector('.issue.selected [data-transition=unconfirm]', function () { - test.assertExists('.issue.selected [data-transition=unconfirm]'); - test.assertExists('.issue.selected [data-transition=resolve]'); - test.assertExists('.issue.selected [data-transition=falsepositive]'); - lib.clearRequestMock(this.showMock); - this.showMock = lib.mockRequestFromFile('/api/issues/show*', 'show-open.json'); - casper.click('.issue.selected [data-transition=unconfirm]'); - }); - }) - - .then(function () { - casper.waitForSelector('.issue.selected [data-transition=confirm]', function () { - test.assertExists('.issue.selected [data-transition=resolve]'); - test.assertExists('.issue.selected [data-transition=falsepositive]'); - lib.clearRequestMock(this.showMock); - this.showMock = lib.mockRequestFromFile('/api/issues/show*', 'show-resolved.json'); - casper.click('.issue.selected [data-transition=resolve]'); - }); - }) - - .then(function () { - casper.waitForSelector('.issue.selected [data-transition=reopen]', function () { - lib.clearRequestMock(this.showMock); - this.showMock = lib.mockRequestFromFile('/api/issues/show*', 'show-open.json'); - casper.click('.issue.selected [data-transition=reopen]'); - }); + casper.waitForSelector('.issue.selected .js-issue-transition'); }) .then(function () { - casper.waitForSelector('.issue.selected [data-transition=confirm]', function () { - test.assertExists('.issue.selected [data-transition=confirm]'); - test.assertExists('.issue.selected [data-transition=resolve]'); - test.assertExists('.issue.selected [data-transition=falsepositive]'); - lib.clearRequestMock(this.showMock); - this.showMock = lib.mockRequestFromFile('/api/issues/show*', 'show-resolved.json'); - casper.click('.issue.selected [data-transition=falsepositive]'); - }); + casper.click('.issue.selected .js-issue-transition'); + casper.waitForSelector('.issue-action-option'); }) .then(function () { - casper.waitForSelector('.issue.selected [data-transition=reopen]', function () { - test.assertExists('.issue.selected [data-transition=reopen]'); - }); + test.assertExists('.issue-action-option[data-value=unconfirm]'); + test.assertExists('.issue-action-option[data-value=resolve]'); + test.assertExists('.issue-action-option[data-value=falsepositive]'); }) .run(function () { diff --git a/server/sonar-web/src/main/less/components/issues.less b/server/sonar-web/src/main/less/components/issues.less index e4029858dde..6db3500bfb8 100644 --- a/server/sonar-web/src/main/less/components/issues.less +++ b/server/sonar-web/src/main/less/components/issues.less @@ -64,30 +64,10 @@ font-weight: 500; } -.issue-tags { - padding-left: @leftPadding; - line-height: 1.5; - font-size: @baseFontSize; -} - -.issue-tag-list { - display: inline-block; - - .icon-tags { - color: @secondFontColor; - } -} - -.issue-tags-change { - cursor: pointer; -} - -.issue-tag-edit { - display: none; -} - -.issue-tag-edit-cancel { - vertical-align: middle; +.issue-rule { + font-size: 11px; + font-weight: 400; + .link-no-underline; } .issue-component { -- 2.39.5