'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'
IssuePopup
+ TransitionsFormView
AssignFormView
CommentFormView
PlanFormView
SetSeverityFormView
MoreActionsView
RuleOverlay
+ TagsFormView
) ->
'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: ->
.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) ->
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
onRender: ->
super
- @renderAssignees()
+ @renderTags()
setTimeout (=> @$('input').focus()), 100
- renderAssignees: ->
+ renderTags: ->
@$('.issue-action-option').remove()
@getAssignees().forEach @renderAssignee, @
@selectInitialOption()
@assignees = users.map (user) ->
id: user.login
text: user.name
- @renderAssignees()
+ @renderTags()
getAssignees: ->
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) ->
submit: (severity) ->
- _severity = @getSeverity()
+ _severity = @getTransition()
return if severity == _severity
p = window.process.addBackgroundProcess()
@model.set severity: severity
--- /dev/null
+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') || []
--- /dev/null
+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
bindShortcuts: ->
- doTransition = (transition) =>
- selectedIssueView = @getSelectedIssueEl()
- return unless selectedIssueView
- selectedIssueView.find("[data-transition=#{transition}]").click()
-
doAction = (action) =>
selectedIssueView = @getSelectedIssueEl()
return unless selectedIssueView
@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'
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?
@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'
--- /dev/null
+<a href="#" class="issue-action-option" data-value="{{tag}}" data-text="{{tag}}"
+ {{#if selected}}data-selected{{/if}}>
+
+ {{#if selected}}
+ <i class="icon-checkbox icon-checkbox-checked"></i>
+ {{else}}
+ <i class="icon-checkbox"></i>
+ {{/if}}
+
+ {{#if custom}}
+ + {{tag}}
+ {{else}}
+ {{tag}}
+ {{/if}}
+</a>
--- /dev/null
+<div class="issue-action-options">
+ <i class="icon-search issue-action-options-search-icon"></i>
+ <input class="issue-action-options-search" type="text" placeholder="Search..." value="{{query}}">
+</div>
+
+<div class="bubble-popup-arrow"></div>
--- /dev/null
+<div class="issue-action-options">
+ {{#each transitions}}
+ <a href="#" class="issue-action-option js-issue-transition" data-value="{{this}}">
+ {{t 'issue.transition' this}}
+ </a>
+ {{/each}}
+</div>
+
+<div class="bubble-popup-arrow"></div>
<tr>
<td>
<div class="issue-message">{{message}}</div>
+ <div class="issue-message">
+ <a class="issue-rule js-issue-rule">{{ruleName}}</a>
+ </div>
</td>
<td class="issue-table-meta-cell">
<div class="issue-meta-list">
- <div class="issue-meta">
- <a class="issue-action js-issue-rule">Rule</a>
- </div>
-
{{#if debt}}
<div class="issue-meta">
<span class="issue-meta-label">{{t 'issue.debt'}} {{debt}}</span>
</div>
<div class="issue-meta">
- {{statusHelper status resolution}}
+ {{#notEmpty transitions}}
+ <a class="issue-action issue-action-with-options js-issue-transition">
+ <span class="issue-meta-label">{{statusHelper status resolution}}</span> <i
+ class="icon-dropdown"></i>
+ </a>
+ {{else}}
+ {{statusHelper status resolution}}
+ {{/notEmpty}}
</div>
- {{#notEmpty transitions}}
- <div class="issue-meta">
- {{#each transitions}}
- <a class="issue-action js-issue-transition" data-transition="{{this}}">
- <span class="issue-meta-label">{{t 'issue.transition' this}}</span>
- </a>
- {{/each}}
- </div>
- {{/notEmpty}}
-
<div class="issue-meta">
{{#inArray actions "assign"}}
<a class="issue-action issue-action-with-options js-issue-assign">
{{#inArray actions "comment"}}
<div class="issue-meta">
<a class="issue-action js-issue-comment"><span
- class="issue-meta-label">{{t 'issue.comment.formlink' }}</span></a>
+ class="issue-meta-label"><i class="icon-comment"></i></span></a>
</div>
{{/inArray}}
</td>
<td class="issue-table-meta-cell">
- <div class="issue-tags">
- <span class="issue-tag-list {{#inArray actions "set_tags"}}js-issue-edit-tags{{/inArray}}">
- <i class="icon-tags"></i>
- <span class="issue-meta">{{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}}</span>
- </span>
+ <div class="issue-meta js-issue-tags">
{{#inArray actions "set_tags"}}
- <span class="issue-meta issue-tag-edit">
- <input class="issue-tag-input" type="text" value="{{#if tags}}{{join tags ','}}{{/if}}">
- <span class="button-group">
- <button class="issue-tag-edit-done">{{t 'Done'}}</button>
- </span>
- <a class="issue-tag-edit-cancel">{{t 'cancel'}}</a>
+ <a class="issue-action issue-action-with-options js-issue-edit-tags">
+ <span>
+ <i class="icon-tags"></i> <span>{{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}}</span>
+ </span> <i class="icon-dropdown"></i>
+ </a>
+ {{else}}
+ <span>
+ <i class="icon-tags"></i> <span>{{#if tags}}{{join tags ', '}}{{else}}{{t 'issue.no_tag'}}{{/if}}</span>
</span>
{{/inArray}}
</div>
</ul>
<p>To control selected issue</p>
<ul>
- <li><span class="shortcut-button">c</span> to confirm/unconfirm</li>
- <li><span class="shortcut-button">r</span> to resolve/reopen</li>
- <li><span class="shortcut-button">f</span> to mark as false positive</li>
- <li><span class="shortcut-button">u</span> to mute</li>
+ <li><span class="shortcut-button">f</span> to do a transition</li>
<li><span class="shortcut-button">a</span> to assign</li>
<li><span class="shortcut-button">m</span> to assign to the current user</li>
<li><span class="shortcut-button">p</span> to plan</li>
.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');
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 () {
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 () {
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 {