aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/coffee/coding-rules
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2014-07-11 17:10:13 +0200
committerSimon Brandhof <simon.brandhof@sonarsource.com>2014-07-11 17:10:13 +0200
commit1de4e949ef73b086e5ff5cef2b39fdb3c3553d41 (patch)
tree5f53797f1a21cc7854db1db5f4a4010322e8db45 /server/sonar-web/src/main/coffee/coding-rules
parentd67483d12ef1768adda8f87db349915891f3b19a (diff)
downloadsonarqube-1de4e949ef73b086e5ff5cef2b39fdb3c3553d41.tar.gz
sonarqube-1de4e949ef73b086e5ff5cef2b39fdb3c3553d41.zip
SONAR-5408 extract sonar-web from sonar-server
Diffstat (limited to 'server/sonar-web/src/main/coffee/coding-rules')
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/app.coffee511
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/layout.coffee90
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/mockjax.coffee364
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/router.coffee37
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/actions-view.coffee64
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee55
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee122
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-custom-rule-creation-view.coffee175
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-custom-rule-view.coffee42
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-custom-rules-view.coffee16
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee101
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee15
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-view.coffee305
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-facets-view.coffee49
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-empty-view.coffee12
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-item-view.coffee23
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-view.coffee93
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee124
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filter-bar-view.coffee76
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/activation-filter-view.coffee30
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/active-severities-filter-view.coffee7
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/characteristic-filter-view.coffee12
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/inheritance-filter-view.coffee22
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/language-filter-view.coffee45
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/profile-dependent-filter-view.coffee57
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee61
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/query-filter-view.coffee76
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/repository-filter-view.coffee54
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/filters/tag-filter-view.coffee37
-rw-r--r--server/sonar-web/src/main/coffee/coding-rules/views/header-view.coffee18
30 files changed, 2693 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/coffee/coding-rules/app.coffee b/server/sonar-web/src/main/coffee/coding-rules/app.coffee
new file mode 100644
index 00000000000..103e12af957
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/app.coffee
@@ -0,0 +1,511 @@
+requirejs.config
+ baseUrl: "#{baseUrl}/js"
+
+ paths:
+ 'backbone': 'third-party/backbone'
+ 'backbone.marionette': 'third-party/backbone.marionette'
+ 'handlebars': 'third-party/handlebars'
+ 'jquery.mockjax': 'third-party/jquery.mockjax'
+
+ shim:
+ 'backbone.marionette':
+ deps: ['backbone']
+ exports: 'Marionette'
+ 'backbone':
+ exports: 'Backbone'
+ 'handlebars':
+ exports: 'Handlebars'
+
+
+requirejs [
+ 'backbone', 'backbone.marionette',
+
+ 'coding-rules/layout',
+ 'coding-rules/router',
+
+ # views
+ 'coding-rules/views/header-view',
+ 'coding-rules/views/actions-view',
+ 'coding-rules/views/filter-bar-view',
+ 'coding-rules/views/coding-rules-list-view',
+ 'coding-rules/views/coding-rules-detail-view',
+ 'coding-rules/views/coding-rules-bulk-change-view',
+ 'coding-rules/views/coding-rules-quality-profile-activation-view',
+ 'coding-rules/views/coding-rules-bulk-change-dropdown-view',
+ 'coding-rules/views/coding-rules-facets-view',
+ 'coding-rules/views/coding-rules-custom-rule-creation-view',
+
+ # filters
+ 'navigator/filters/base-filters',
+ 'navigator/filters/choice-filters',
+ 'navigator/filters/string-filters',
+ 'navigator/filters/date-filter-view',
+ 'navigator/filters/read-only-filters',
+ 'coding-rules/views/filters/query-filter-view',
+ 'coding-rules/views/filters/quality-profile-filter-view',
+ 'coding-rules/views/filters/inheritance-filter-view',
+ 'coding-rules/views/filters/active-severities-filter-view',
+ 'coding-rules/views/filters/activation-filter-view',
+ 'coding-rules/views/filters/characteristic-filter-view',
+ 'coding-rules/views/filters/repository-filter-view',
+ 'coding-rules/views/filters/tag-filter-view',
+ 'coding-rules/views/filters/language-filter-view',
+
+ 'coding-rules/mockjax',
+ 'common/handlebars-extensions'
+], (
+ Backbone, Marionette,
+
+ CodingRulesLayout,
+ CodingRulesRouter,
+
+ # views
+ CodingRulesHeaderView,
+ CodingRulesActionsView,
+ CodingRulesFilterBarView,
+ CodingRulesListView,
+ CodingRulesDetailView,
+ CodingRulesBulkChangeView,
+ CodingRulesQualityProfileActivationView,
+ CodingRulesBulkChangeDropdownView,
+ CodingRulesFacetsView,
+ CodingRulesCustomRuleCreationView,
+
+ # filters
+ BaseFilters,
+ ChoiceFilters,
+ StringFilterView,
+ DateFilterView,
+ ReadOnlyFilterView,
+ QueryFilterView,
+ QualityProfileFilterView,
+ InheritanceFilterView,
+ ActiveSeveritiesFilterView,
+ ActivationFilterView,
+ CharacteristicFilterView,
+ RepositoryFilterView,
+ TagFilterView,
+ LanguageFilterView
+) ->
+
+ # Create a generic error handler for ajax requests
+ jQuery.ajaxSetup
+ error: (jqXHR) ->
+ text = jqXHR.responseText
+ errorBox = jQuery('.modal-error')
+ if jqXHR.responseJSON?.errors?
+ text = _.pluck(jqXHR.responseJSON.errors, 'msg').join '. '
+ else
+ text = t 'default_error_message'
+ if errorBox.length > 0
+ errorBox.show().text text
+ else
+ alert text
+
+
+ # Add html class to mark the page as navigator page
+ jQuery('html').addClass('navigator-page coding-rules-page');
+
+
+ # Create an Application
+ App = new Marionette.Application
+
+
+ App.getQuery = (includeFacetsQuery = true) ->
+ query = @filterBarView.getQuery()
+ if includeFacetsQuery and @codingRulesFacetsView
+ _.extend query, @codingRulesFacetsView.getQuery()
+ query
+
+
+ App.restoreSorting = (params) ->
+ sort = _.findWhere(params, key: 'sort')
+ asc = _.findWhere(params, key: 'asc')
+
+ if (sort && asc)
+ @codingRules.sorting =
+ sort: sort.value
+ asc: asc.value =='true'
+
+
+ App.restoreDefaultSorting = ->
+ params = []
+ params.push(key: 'sort', value: 'createdAt')
+ params.push(key: 'asc', value: false)
+ @restoreSorting params
+
+
+ App.storeQuery = (query, sorting) ->
+ if sorting && sorting.sort
+ _.extend query,
+ s: sorting.sort
+ asc: '' + sorting.asc
+ queryString = _.map query, (v, k) -> "#{k}=#{encodeURIComponent(v)}"
+ @router.navigate queryString.join('|'), replace: true
+
+
+
+ App.fetchList = (firstPage, fromFacets = false) ->
+ pristineQuery = @getQuery(false)
+ query = @getQuery(fromFacets)
+
+ fetchQuery = _.extend { p: @pageIndex, ps: 25, facets: not fromFacets }, query
+
+ if @codingRules.sorting && @codingRules.sorting.sort
+ _.extend fetchQuery,
+ s: @codingRules.sorting.sort,
+ asc: @codingRules.sorting.asc
+
+ @storeQuery pristineQuery, @codingRules.sorting
+
+ # Optimize requested fields
+ _.extend fetchQuery, f: 'name,lang,status'
+
+ if @codingRulesListView
+ scrollOffset = jQuery('.navigator-results')[0].scrollTop
+ else
+ scrollOffset = 0
+
+ @layout.showSpinner 'resultsRegion'
+ @layout.showSpinner 'facetsRegion' unless fromFacets || !firstPage
+
+
+ jQuery.ajax
+ url: "#{baseUrl}/api/rules/search"
+ data: fetchQuery
+ .done (r) =>
+ _.map(r.rules, (rule) ->
+ rule.language = App.languages[rule.lang]
+ )
+
+ @codingRules.paging =
+ total: r.total
+ pageIndex: r.p
+ pageSize: r.ps
+ pages: 1 + (r.total / r.ps)
+
+ if @codingRulesListView
+ @codingRulesListView.close()
+
+ if firstPage
+ @codingRules.reset r.rules
+ @codingRulesListView = new CodingRulesListView
+ app: @
+ collection: @codingRules
+ else
+ @codingRulesListView.unbindEvents()
+ @codingRules.add r.rules
+
+ @layout.resultsRegion.show @codingRulesListView
+
+
+ if @codingRules.isEmpty()
+ @layout.detailsRegion.reset()
+ else if firstPage
+ @codingRulesListView.selectFirst()
+ else
+ @codingRulesListView.selectCurrent()
+
+ unless firstPage
+ jQuery('.navigator-results')[0].scrollTop = scrollOffset
+
+ unless fromFacets
+ @codingRulesFacetsView = new CodingRulesFacetsView
+ app: @
+ collection: new Backbone.Collection r.facets, comparator: 'property'
+ @layout.facetsRegion.show @codingRulesFacetsView
+
+ @layout.onResize()
+
+
+
+ App.facetLabel = (property, value) ->
+ return value unless App.facetPropertyToLabels[property]
+ App.facetPropertyToLabels[property](value)
+
+
+ App.fetchFirstPage = (fromFacets = false) ->
+ @pageIndex = 1
+ App.fetchList true, fromFacets
+
+
+ App.fetchNextPage = (fromFacets = true) ->
+ if @pageIndex < @codingRules.paging.pages
+ @pageIndex++
+ App.fetchList false, fromFacets
+
+
+ App.getQualityProfile = ->
+ value = @qualityProfileFilter.get('value')
+ if value? && value.length == 1 then value[0] else null
+
+
+ App.getQualityProfilesForLanguage = (language_key) ->
+ _.filter App.qualityProfiles, (p) => p.lang == language_key
+
+ App.getQualityProfileByKey = (profile_key) ->
+ _.findWhere App.qualityProfiles, key: profile_key
+
+ App.showRule = (ruleKey) ->
+ App.layout.showSpinner 'detailsRegion'
+ jQuery.ajax
+ url: "#{baseUrl}/api/rules/show"
+ data:
+ key: ruleKey
+ actives: true
+ .done (r) =>
+ rule = new Backbone.Model(r.rule)
+ App.codingRulesQualityProfileActivationView.rule = rule
+ App.detailView = new CodingRulesDetailView
+ app: App
+ model: rule
+ actives: r.actives
+ App.layout.detailsRegion.show App.detailView
+
+
+ App.manualRepository = ->
+ key: 'manual'
+ name: 'Manual Rules'
+ language: 'none'
+
+
+ # Construct layout
+ App.addInitializer ->
+ @layout = new CodingRulesLayout app: @
+ jQuery('#content').append @layout.render().el
+ @layout.onResize()
+
+
+ # Construct header
+ App.addInitializer ->
+ @codingRulesHeaderView = new CodingRulesHeaderView app: @
+ @layout.headerRegion.show @codingRulesHeaderView
+
+
+ # Define coding rules
+ App.addInitializer ->
+ @codingRules = new Backbone.Collection
+ @restoreDefaultSorting()
+
+
+ # Construct status bar
+ App.addInitializer ->
+ @codingRulesActionsView = new CodingRulesActionsView
+ app: @
+ collection: @codingRules
+ @layout.actionsRegion.show @codingRulesActionsView
+
+
+ # Construct bulk change views
+ App.addInitializer ->
+ @codingRulesBulkChangeView = new CodingRulesBulkChangeView app: @
+ @codingRulesBulkChangeDropdownView = new CodingRulesBulkChangeDropdownView app: @
+
+
+ # Construct quality profile activation view
+ App.addInitializer ->
+ @codingRulesQualityProfileActivationView = new CodingRulesQualityProfileActivationView app: @
+
+
+ # Construct custom rule creation view
+ App.addInitializer ->
+ @codingRulesCustomRuleCreationView = new CodingRulesCustomRuleCreationView app: @
+
+
+ # Define filters
+ App.addInitializer ->
+ @filters = new BaseFilters.Filters
+
+ @queryFilter = new BaseFilters.Filter
+ property: 'q'
+ type: QueryFilterView
+ size: 50
+ @filters.add @queryFilter
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.severity'
+ property: 'severities'
+ type: ChoiceFilters.ChoiceFilterView
+ optional: true
+ choices:
+ 'BLOCKER': t 'severity.BLOCKER'
+ 'CRITICAL': t 'severity.CRITICAL'
+ 'MAJOR': t 'severity.MAJOR'
+ 'MINOR': t 'severity.MINOR'
+ 'INFO': t 'severity.INFO'
+ choiceIcons:
+ 'BLOCKER': 'severity-blocker'
+ 'CRITICAL': 'severity-critical'
+ 'MAJOR': 'severity-major'
+ 'MINOR': 'severity-minor'
+ 'INFO': 'severity-info'
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.tag'
+ property: 'tags'
+ type: TagFilterView
+ optional: true
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.characteristic'
+ property: 'debt_characteristics'
+ type: CharacteristicFilterView
+ choices: @characteristics
+ multiple: false
+ optional: true
+
+ @qualityProfileFilter = new BaseFilters.Filter
+ name: t 'coding_rules.filters.quality_profile'
+ property: 'qprofile'
+ type: QualityProfileFilterView
+ app: @
+ choices: @qualityProfiles
+ multiple: false
+ @filters.add @qualityProfileFilter
+
+ @activationFilter = new BaseFilters.Filter
+ name: t 'coding_rules.filters.activation'
+ property: 'activation'
+ type: ActivationFilterView
+ enabled: false
+ optional: true
+ multiple: false
+ qualityProfileFilter: @qualityProfileFilter
+ choices:
+ true: t 'coding_rules.filters.activation.active'
+ false: t 'coding_rules.filters.activation.inactive'
+ @filters.add @activationFilter
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.active_severity'
+ property: 'active_severities'
+ type: ActiveSeveritiesFilterView
+ enabled: false
+ optional: true
+ qualityProfileFilter: @qualityProfileFilter
+ choices:
+ 'BLOCKER': t 'severity.BLOCKER'
+ 'CRITICAL': t 'severity.CRITICAL'
+ 'MAJOR': t 'severity.MAJOR'
+ 'MINOR': t 'severity.MINOR'
+ 'INFO': t 'severity.INFO'
+ choiceIcons:
+ 'BLOCKER': 'severity-blocker'
+ 'CRITICAL': 'severity-critical'
+ 'MAJOR': 'severity-major'
+ 'MINOR': 'severity-minor'
+ 'INFO': 'severity-info'
+
+ @languageFilter = new BaseFilters.Filter
+ name: t 'coding_rules.filters.language'
+ property: 'languages'
+ type: LanguageFilterView
+ app: @
+ choices: @languages
+ optional: true
+ @filters.add @languageFilter
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.availableSince'
+ property: 'available_since'
+ type: DateFilterView
+ enabled: false
+ optional: true
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.inheritance'
+ property: 'inheritance'
+ type: InheritanceFilterView
+ enabled: false
+ optional: true
+ multiple: false
+ qualityProfileFilter: @qualityProfileFilter
+ choices:
+ 'NONE': t 'coding_rules.filters.inheritance.not_inherited'
+ 'INHERITED': t 'coding_rules.filters.inheritance.inherited'
+ 'OVERRIDES': t 'coding_rules.filters.inheritance.overriden'
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.repository'
+ property: 'repositories'
+ type: RepositoryFilterView
+ enabled: false
+ optional: true
+ app: @
+ choices: @repositories
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.status'
+ property: 'statuses'
+ type: ChoiceFilters.ChoiceFilterView
+ enabled: false
+ optional: true
+ choices: @statuses
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.template'
+ property: 'is_template'
+ type: ChoiceFilters.ChoiceFilterView
+ optional: true
+ multiple: false
+ choices:
+ 'true': t 'coding_rules.filters.template.is_template'
+ 'false': t 'coding_rules.filters.template.is_not_template'
+
+ @filters.add new BaseFilters.Filter
+ name: t 'coding_rules.filters.key'
+ property: 'rule_key'
+ type: ReadOnlyFilterView
+ enabled: false
+ inactive: true
+ optional: true
+
+
+ @filterBarView = new CodingRulesFilterBarView
+ app: @
+ collection: @filters,
+ extra: sort: '', asc: false
+ @layout.filtersRegion.show @filterBarView
+
+
+ # Start router
+ App.addInitializer ->
+ @router = new CodingRulesRouter app: @
+ Backbone.history.start()
+
+
+ # Call app before start the application
+ appXHR = jQuery.ajax
+ url: "#{baseUrl}/api/rules/app"
+ .done (r) ->
+ App.appState = new Backbone.Model
+ App.state = new Backbone.Model
+ App.canWrite = r.canWrite
+ App.qualityProfiles = _.sortBy r.qualityprofiles, ['name', 'lang']
+ App.languages = _.extend r.languages, none: 'None'
+ _.map App.qualityProfiles, (profile) ->
+ profile.language = App.languages[profile.lang]
+ App.repositories = r.repositories
+ App.repositories.push App.manualRepository()
+ App.statuses = r.statuses
+ App.characteristics = r.characteristics
+
+ App.facetPropertyToLabels =
+ 'languages': (value) -> App.languages[value]
+ 'repositories': (value) ->
+ repo = _.findWhere(App.repositories, key: value)
+ other_repo_with_same_name = _.find(App.repositories, (repos) -> repos.name == repo.name && repos.key != repo.key)
+ if other_repo_with_same_name
+ repo.name + ' - ' + App.languages[repo.language]
+ else
+ repo.name
+
+ # Message bundles
+ l10nXHR = window.requestMessages()
+
+ jQuery.when(l10nXHR, appXHR).done ->
+ # Remove the initial spinner
+ jQuery('#coding-rules-page-loader').remove()
+
+ # Start the application
+ App.start()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/layout.coffee b/server/sonar-web/src/main/coffee/coding-rules/layout.coffee
new file mode 100644
index 00000000000..8008248f670
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/layout.coffee
@@ -0,0 +1,90 @@
+define [
+ 'backbone.marionette',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class AppLayout extends Marionette.Layout
+ className: 'navigator coding-rules-navigator'
+ template: Templates['coding-rules-layout']
+ storageKey: 'codingRulesResultsWidth'
+
+
+ regions:
+ headerRegion: '.navigator-header'
+ actionsRegion: '.navigator-actions'
+ resultsRegion: '.navigator-results'
+ detailsRegion: '.navigator-details'
+ filtersRegion: '.navigator-filters'
+ facetsRegion: '.navigator-facets'
+
+
+ ui:
+ side: '.navigator-side'
+ results: '.navigator-results'
+ details: '.navigator-details'
+ resizer: '.navigator-resizer'
+
+
+ initialize: ->
+ jQuery(window).on 'resize', => @onResize()
+
+ @isResize = false
+ jQuery('body').on 'mousemove', (e) => @processResize(e)
+ jQuery('body').on 'mouseup', => @stopResize()
+
+
+ onRender: ->
+ @ui.resizer.on 'mousedown', (e) => @startResize(e)
+
+ resultsWidth = localStorage.getItem @storageKey
+ if resultsWidth
+ @$(@resultsRegion.el).width +resultsWidth
+ @ui.side.width +resultsWidth + 20
+
+
+ onResize: ->
+ footerEl = jQuery('#footer')
+ footerHeight = footerEl.outerHeight true
+
+ resultsEl = @ui.results
+ resultsHeight = jQuery(window).height() - resultsEl.offset().top -
+ parseInt(resultsEl.css('margin-bottom'), 10) - footerHeight
+ resultsEl.height resultsHeight
+
+ detailsEl = @ui.details
+ detailsWidth = jQuery(window).width() - detailsEl.offset().left -
+ parseInt(detailsEl.css('margin-right'), 10)
+ detailsHeight = jQuery(window).height() - detailsEl.offset().top -
+ parseInt(detailsEl.css('margin-bottom'), 10) - footerHeight
+ detailsEl.width(detailsWidth).height detailsHeight
+
+
+ showSpinner: (region) ->
+ @[region].show new Marionette.ItemView
+ template: _.template('<i class="spinner"></i>')
+
+
+ startResize: (e) ->
+ @isResize = true
+ @originalWidth = @ui.results.width()
+ @x = e.clientX
+ jQuery('html').attr('unselectable', 'on').css('user-select', 'none').on('selectstart', false)
+
+
+ processResize: (e) ->
+ if @isResize
+ delta = e.clientX - @x
+ @$(@resultsRegion.el).width @originalWidth + delta
+ @ui.side.width @originalWidth + 20 + delta
+ localStorage.setItem @storageKey, @ui.results.width()
+ @onResize()
+
+
+ stopResize: ->
+ if @isResize
+ jQuery('html').attr('unselectable', 'off').css('user-select', 'text').off('selectstart')
+ @isResize = false
+ true
diff --git a/server/sonar-web/src/main/coffee/coding-rules/mockjax.coffee b/server/sonar-web/src/main/coffee/coding-rules/mockjax.coffee
new file mode 100644
index 00000000000..30f37ba626b
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/mockjax.coffee
@@ -0,0 +1,364 @@
+define ['jquery.mockjax'], ->
+
+ jQuery.mockjaxSettings.contentType = 'text/json';
+ jQuery.mockjaxSettings.responseTime = 250;
+
+ # GET /api/codingrules/app
+ jQuery.mockjax
+ url: "#{baseUrl}/api/codingrules/app"
+ responseText: JSON.stringify
+ qualityprofiles: [
+ { key: 'sonarway', name: 'Sonar Way', lang: 'Java', parent: null },
+ { key: 'qualityprofile1', name: 'Quality Profile 1', lang: 'Java', parent: 'sonarway' },
+ { key: 'qualityprofile2', name: 'Quality Profile 2', lang: 'JavaScript', parent: 'sonarway' },
+ { key: 'qualityprofile3', name: 'Quality Profile 3', lang: 'Java', parent: null },
+ ]
+ languages:
+ java: 'Java'
+ javascript: 'JavaScript'
+ repositories:
+ 'checkstyle': 'Checkstyle'
+ 'common-java': 'Common SonarQube'
+ 'findbugs': 'FindBugs'
+ 'pmd': 'PMD'
+ 'pmd-unit-tests': 'PMD Unit Tests'
+ 'squid': 'SonarQube'
+ statuses:
+ 'BETA': 'Beta'
+ 'DEPRECATED': 'Deprecated'
+ 'READY': 'Ready'
+ tags:
+ 'brain-overload': 'brain-overload'
+ 'bug': 'bug'
+ 'comment': 'comment'
+ 'convention': 'convention'
+ 'error-handling': 'error-handling'
+ 'formatting': 'formatting'
+ 'java8': 'java8'
+ 'multithreading': 'multithreading'
+ 'naming': 'naming'
+ 'pitfall': 'pitfall'
+ 'security': 'security'
+ 'size': 'size'
+ 'unused': 'unused'
+ 'unused-code': 'unused-code'
+ characteristics:
+ '1469': 'Changeability'
+ '1441': 'Changeability: Architecture related changeability'
+ '1470': 'Changeability: Data related changeability'
+ '1475': 'Changeability: Logic related changeability'
+ '1392': 'Efficiency'
+ '1377': 'Efficiency: Memory use'
+ '2965': 'Efficiency: Network use'
+ '1393': 'Efficiency: Processor use'
+ '1154': 'Maintainability'
+ '1022': 'Maintainability: Readability'
+ '1155': 'Maintainability: Understandability'
+ '988': 'Portability'
+ '977': 'Portability: Compiler related portability'
+ '989': 'Portability: Hardware related portability'
+ '994': 'Portability: Language related portability'
+ '1000': 'Portability: OS related portability'
+ '1006': 'Portability: Software related portability'
+ '1021': 'Portability: Time zone related portability'
+ '1551': 'Reliability'
+ '1496': 'Reliability: Architecture related reliability'
+ '1552': 'Reliability: Data related reliability'
+ '1596': 'Reliability: Exception handling'
+ '1622': 'Reliability: Fault tolerance'
+ '1629': 'Reliability: Instruction related reliability'
+ '1759': 'Reliability: Logic related reliability'
+ '2948': 'Reliability: Resource'
+ '1874': 'Reliability: Synchronization related reliability'
+ '1925': 'Reliability: Unit tests'
+ '975': 'Reusability'
+ '974': 'Reusability: Modularity'
+ '976': 'Reusability: Transportability'
+ '1345': 'Security'
+ '1335': 'Security: API abuse'
+ '1346': 'Security: Errors'
+ '1349': 'Security: Input validation and representation'
+ '1364': 'Security: Security features'
+ '1933': 'Testability'
+ '1932': 'Testability: Integration level testability'
+ '1934': 'Testability: Unit level testability'
+ messages:
+ 'all': 'All'
+ 'any': 'Any'
+ 'apply': 'Apply'
+ 'are_you_sure': 'Are you sure?'
+ 'bold': 'Bold'
+ 'bulk_change': 'Bulk Change'
+ 'bulleted_point': 'Bulleted point'
+ 'cancel': 'Cancel'
+ 'change': 'Change'
+ 'code': 'Code'
+ 'delete': 'Delete'
+ 'done': 'Done'
+ 'edit': 'Edit'
+ 'markdown.helplink': 'Markdown Help'
+ 'moreCriteria': '+ More Criteria'
+ 'save': 'Save'
+ 'search_verb': 'Search'
+ 'severity': 'Severity'
+ 'update': 'Update'
+
+ 'severity.BLOCKER': 'Blocker'
+ 'severity.CRITICAL': 'Critical'
+ 'severity.MAJOR': 'Major'
+ 'severity.MINOR': 'Minor'
+ 'severity.INFO': 'Info'
+
+ 'coding_rules.activate': 'Activate'
+ 'coding_rules.activate_in': 'Activate In'
+ 'coding_rules.activate_in_quality_profile': 'Activate In Quality Profile'
+ 'coding_rules.activate_in_all_quality_profiles': 'Activate In All {0} Profiles'
+ 'coding_rules.add_note': 'Add Note'
+ 'coding_rules.available_since': 'Available Since'
+ 'coding_rules.bulk_change': 'Bulk Change'
+ 'coding_rules.change_severity': 'Change Severity'
+ 'coding_rules.change_severity_in': 'Change Severity In'
+ 'coding_rules.change_details': 'Change Details of Quality Profile'
+ 'coding_rules.extend_description': 'Extend Description'
+ 'coding_rules.deactivate_in': 'Deactivate In'
+ 'coding_rules.deactivate': 'Deactivate'
+ 'coding_rules.deactivate_in_quality_profile': 'Deactivate In Quality Profile'
+ 'coding_rules.deactivate_in_all_quality_profiles': 'Deactivate In All {0} Profiles'
+ 'coding_rules.found': 'Found'
+ 'coding_rules.inherits': '"{0}" inherits "{1}"'
+ 'coding_rules.key': 'Key:'
+ 'coding_rules.new_search': 'New Search'
+ 'coding_rules.no_results': 'No Coding Rules'
+ 'coding_rules.no_tags': 'No tags'
+ 'coding_rules.order': 'Order'
+ 'coding_rules.ordered_by': 'Ordered By'
+ 'coding_rules.original': 'Original:'
+ 'coding_rules.page': 'Coding Rules'
+ 'coding_rules.parameters': 'Parameters'
+ 'coding_rules.parameters.default_value': 'Default Value:'
+ 'coding_rules.permalink': 'Permalink'
+ 'coding_rules.quality_profiles': 'Quality Profiles'
+ 'coding_rules.quality_profile': 'Quality Profile'
+ 'coding_rules.repository': 'Repository:'
+ 'coding_rules.revert_to_parent_definition': 'Revert to Parent Definition'
+ 'coding_rules._rules': 'rules'
+ 'coding_rules.select_tag': 'Select Tag'
+
+ 'coding_rules.filters.activation': 'Activation'
+ 'coding_rules.filters.activation.active': 'Active'
+ 'coding_rules.filters.activation.inactive': 'Inactive'
+ 'coding_rules.filters.activation.help': 'Activation criterion is available when a quality profile is selected'
+ 'coding_rules.filters.availableSince': 'Available Since'
+ 'coding_rules.filters.characteristic': 'Characteristic'
+ 'coding_rules.filters.description': 'Description'
+ 'coding_rules.filters.quality_profile': 'Quality Profile'
+ 'coding_rules.filters.inheritance': 'Inheritance'
+ 'coding_rules.filters.inheritance.inactive': 'Inheritance criterion is available when an inherited quality profile is selected'
+ 'coding_rules.filters.inheritance.not_inherited': 'Not Inherited'
+ 'coding_rules.filters.inheritance.inherited': 'Inherited'
+ 'coding_rules.filters.inheritance.overriden': 'Overriden'
+ 'coding_rules.filters.key': 'Key'
+ 'coding_rules.filters.language': 'Language'
+ 'coding_rules.filters.name': 'Name'
+ 'coding_rules.filters.repository': 'Repository'
+ 'coding_rules.filters.severity': 'Severity'
+ 'coding_rules.filters.status': 'Status'
+ 'coding_rules.filters.tag': 'Tag'
+
+ 'coding_rules.sort.creation_date': 'Creation Date'
+ 'coding_rules.sort.name': 'Name'
+
+
+ # GET /api/codingrules/search
+ jQuery.mockjax
+ url: "#{baseUrl}/api/codingrules/search"
+ responseText: JSON.stringify
+ codingrules: [
+ {
+ name: 'Array designators "[]" should be located after the type in method signatures'
+ language: 'Java'
+ severity: 'MAJOR'
+ status: 'DEPRECATED'
+ },
+ {
+ name: 'Avoid Array Loops'
+ language: 'Java'
+ severity: 'CRITICAL'
+ status: 'READY'
+ },
+ {
+ name: 'Bad practice - Abstract class defines covariant compareTo() method'
+ language: 'Java'
+ severity: 'MAJOR'
+ status: 'READY'
+ },
+ {
+ name: 'Correctness - Use of class without a hashCode() method in a hashed data structure'
+ language: 'Java'
+ severity: 'MINOR'
+ status: 'BETA'
+ },
+ {
+ name: 'Useless Operation On Immutable'
+ language: 'Java'
+ severity: 'MAJOR'
+ status: 'READY'
+ }
+ ]
+ paging:
+ total: 5
+ fTotal: '5'
+ facets: [
+ {
+ name: 'Languages'
+ property: 'languages'
+ values: [
+ { key: 'java', text: 'Java', stat: 45 }
+ { key: 'javascript', text: 'JavaScript', stat: 21 }
+ ]
+ }
+ {
+ name: 'Tags'
+ property: 'tags'
+ values: [
+ { key: 'brain-overload', text: 'brain-overload', stat: 8 }
+ { key: 'bug', text: 'bug', stat: 7 }
+ { key: 'comment', text: 'comment', stat: 7 }
+ { key: 'convention', text: 'convention', stat: 6 }
+ { key: 'error-handling', text: 'error-handling', stat: 5 }
+ ]
+ }
+ {
+ name: 'Repositories'
+ property: 'repositories'
+ values: [
+ { key: 'squid', text: 'SonarQube', stat: 57 }
+ { key: 'pmd', text: 'PMD', stat: 17 }
+ ]
+ }
+ ]
+
+
+
+
+ # GET /api/codingrules/show
+ jQuery.mockjax
+ url: "#{baseUrl}/api/codingrules/show"
+ responseText: JSON.stringify
+ codingrule:
+ name: 'Array designators "[]" should be located after the type in method signatures'
+ language: 'Java'
+ creationDate: '2013-10-15'
+ fCreationDate: 'Oct 15, 2013'
+ status: 'DEPRECATED'
+ repositoryName: 'SonarQube'
+ repositoryKey: 'squid'
+ characteristic: 'Reliability'
+ subcharacteristic: 'Data related reliability'
+ key: 'S1190'
+ parameters: [
+ { key: 'someParameter', type: 'INT', default: 4, description: 'Some parameter description' }
+ { key: 'xpath', type: 'TEXT', description: 'XPath, the XML Path Language, is a query language for selecting nodes from an XML document. In addition, XPath may be used to compute values (e.g., strings, numbers, or Boolean values) from the content of an XML document. XPath was defined by the World Wide Web Consortium (W3C).' }
+ ]
+ description: '''
+ <p>
+ According to the Java Language Specification:
+ </p>
+
+ <pre>For compatibility with older versions of the Java SE platform,
+ the declaration of a method that returns an array is allowed to place (some or all of)
+ the empty bracket pairs that form the declaration of the array type after
+ the formal parameter list. This obsolescent syntax should not be used in new code.
+ </pre>
+
+ <p>The following code snippet illustrates this rule:</p>
+
+ <pre>public int getVector()[] { /* ... */ } // Non-Compliant
+
+ public int[] getVector() { /* ... */ } // Compliant
+
+ public int[] getMatrix()[] { /* ... */ } // Non-Compliant
+
+ public int[][] getMatrix() { /* ... */ } // Compliant
+ </pre>'''
+ extra: '''This note is here <b>only for test purposes</b>.'''
+ extraRaw: '''This note is here *only for test purposes*.'''
+
+ qualityProfiles: [
+ {
+ name: 'SonarWay'
+ key: 'sonarway'
+ severity: 'MINOR'
+ parameters: [
+ { key: 'someParameter', value: 8 }
+ { key: 'xpath', value: '/child::html/child::body/child::*/child::span[attribute::class]' }
+ ]
+ },
+ {
+ name: 'Quality Profile 1'
+ key: 'qualityprofile1'
+ severity: 'MAJOR'
+ parameters: [
+ { key: 'someParameter', value: 6 }
+ { key: 'xpath', value: '/html/body/*/span[@class]' }
+ ]
+ inherits: 'sonarway'
+ }
+ ]
+
+
+
+ # POST /api/codingrules/extend_description
+ jQuery.mockjax
+ url: "#{baseUrl}/api/codingrules/extend_description"
+ responseText: JSON.stringify
+ extra: '''This note is here <i>only for test purposes</i>.'''
+ extraRaw: '''This note is here *only for test purposes*.'''
+
+
+ # POST /api/codingrules/bulk_change
+ jQuery.mockjax
+ url: "#{baseUrl}/api/codingrules/bulk_change"
+
+
+ # POST /api/codingrules/set_tags
+ jQuery.mockjax
+ url: "#{baseUrl}/api/codingrules/set_tags"
+
+
+ # POST /api/codingrules/activate
+ jQuery.mockjax
+ url: "#{baseUrl}/api/codingrules/activate"
+
+
+ # POST /api/codingrules/note
+ jQuery.mockjax
+ url: "#{baseUrl}/api/codingrules/note"
+ responseText: JSON.stringify
+ note:
+ username: 'Admin Admin'
+ html: '''<p>This note is here <b>only for test purposes</b>.</p>'''
+ raw: '''This note is here *only for test purposes*.'''
+ fCreationDate: 'less than a minute'
+
+
+ # GET /api/qualityprofiles/list
+ jQuery.mockjax
+ url: "#{baseUrl}/api/qualityprofiles/list"
+ responseText: JSON.stringify
+ more: false
+ results: [
+ { id: 'sonarway', text: 'Sonar Way', category: 'Java', parent: null },
+ { id: 'qp1', text: 'Quality Profile 1', category: 'Java', parent: 'sonarway' },
+ { id: 'qp2', text: 'Quality Profile 2', category: 'JavaScript', parent: 'sonarway' },
+ { id: 'qp3', text: 'Quality Profile 3', category: 'Java', parent: null },
+ ]
+
+
+ # GET /api/qualityprofiles/show
+ jQuery.mockjax
+ url: "#{baseUrl}/api/qualityprofiles/show"
+ responseText: JSON.stringify
+ qualityprofile:
+ id: 'sonarway', text: 'Sonar Way', category: 'Java', parent: null
+
diff --git a/server/sonar-web/src/main/coffee/coding-rules/router.coffee b/server/sonar-web/src/main/coffee/coding-rules/router.coffee
new file mode 100644
index 00000000000..a6f73769b20
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/router.coffee
@@ -0,0 +1,37 @@
+define [
+ 'backbone',
+], (
+ Backbone,
+) ->
+
+ class AppRouter extends Backbone.Router
+
+ routes:
+ '': 'emptyQuery'
+ ':query': 'index'
+
+
+ initialize: (options) ->
+ @app = options.app
+
+
+ parseQuery: (query, separator) ->
+ (query || '').split(separator || '|').map (t) ->
+ tokens = t.split('=')
+ key: tokens[0], value: decodeURIComponent(tokens[1])
+
+
+ emptyQuery: ->
+ @app.restoreDefaultSorting()
+ @index('')
+
+
+ index: (query) ->
+ params = this.parseQuery(query)
+ @loadResults(params)
+
+
+ loadResults: (params) ->
+ @app.filterBarView.restoreFromQuery(params)
+ @app.restoreSorting(params)
+ @app.fetchFirstPage()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/actions-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/actions-view.coffee
new file mode 100644
index 00000000000..b5f13553de5
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/actions-view.coffee
@@ -0,0 +1,64 @@
+define [
+ 'backbone.marionette'
+ 'templates/coding-rules'
+], (
+ Marionette
+ Templates
+) ->
+
+ class CodingRulesStatusView extends Marionette.ItemView
+ template: Templates['coding-rules-actions']
+
+
+ collectionEvents:
+ 'all': 'render'
+
+
+ ui:
+ orderChoices: '.navigator-actions-order-choices'
+ bulkChange: '.navigator-actions-bulk'
+
+
+ events:
+ 'click .navigator-actions-order': 'toggleOrderChoices'
+ 'click @ui.orderChoices': 'sort'
+ 'click @ui.bulkChange': 'bulkChange'
+
+
+ onRender: ->
+ unless @collection.sorting.sortText
+ while not @collection.sorting.sortText
+ @collection.sorting.sortText = @$('[data-sort=' + @collection.sorting.sort + ']:first').text()
+ @render()
+
+
+ toggleOrderChoices: (e) ->
+ e.stopPropagation()
+ @ui.orderChoices.toggleClass 'open'
+ if @ui.orderChoices.is '.open'
+ jQuery('body').on 'click.coding_rules_actions', =>
+ @ui.orderChoices.removeClass 'open'
+
+
+ sort: (e) ->
+ e.stopPropagation()
+ @ui.orderChoices.removeClass 'open'
+ jQuery('body').off 'click.coding_rules_actions'
+ el = jQuery(e.target)
+ sort = el.data 'sort'
+ asc = el.data 'asc'
+ if sort != null && asc != null
+ @collection.sorting = sort: sort, sortText: el.text(), asc: asc
+ @options.app.fetchFirstPage()
+
+
+ bulkChange: (e) ->
+ e.stopPropagation()
+ @options.app.codingRulesBulkChangeDropdownView.toggle()
+
+
+ serializeData: ->
+ _.extend super,
+ canWrite: @options.app.canWrite
+ paging: @collection.paging
+ sorting: @collection.sorting
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee
new file mode 100644
index 00000000000..27552173ff9
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee
@@ -0,0 +1,55 @@
+define [
+ 'backbone.marionette',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class CodingRulesBulkChangeDropdownView extends Marionette.ItemView
+ className: 'coding-rules-bulk-change-dropdown'
+ template: Templates['coding-rules-bulk-change-dropdown']
+
+
+ events:
+ 'click .coding-rules-bulk-change-dropdown-link': 'doAction'
+
+
+ doAction: (e) ->
+ action = jQuery(e.currentTarget).data 'action'
+ param = jQuery(e.currentTarget).data 'param'
+ @options.app.codingRulesBulkChangeView.show action, param
+
+
+ onRender: ->
+ jQuery('body').append @el
+ jQuery('body').off('click.bulk-change').on 'click.bulk-change', => @hide()
+ @$el.css
+ top: jQuery('.navigator-actions').offset().top + jQuery('.navigator-actions').height() + 1
+ left: jQuery('.navigator-actions').offset().left + jQuery('.navigator-actions').outerWidth() - @$el.outerWidth()
+
+
+ toggle: ->
+ if @$el.is(':visible') then @hide() else @show()
+
+
+ show: ->
+ @render()
+ @$el.show()
+
+
+ hide: ->
+ @$el.hide()
+
+
+ serializeData: ->
+ languages = @options.app.languageFilter.get('value')
+ activationValues = @options.app.activationFilter.get('value') or []
+ qualityProfile = @options.app.getQualityProfile()
+
+ qualityProfile: qualityProfile
+ qualityProfileName: @options.app.qualityProfileFilter.view.renderValue()
+ singleLanguage: _.isArray(languages) and languages.length == 1
+ language: @options.app.languageFilter.view.renderValue()
+ allowActivateOnProfile: qualityProfile and (activationValues.length == 0 or activationValues[0] == 'false')
+ allowDeactivateOnProfile: qualityProfile and (activationValues.length == 0 or activationValues[0] == 'true')
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee
new file mode 100644
index 00000000000..7a7b697c758
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee
@@ -0,0 +1,122 @@
+define [
+ 'backbone.marionette',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class CodingRulesBulkChangeView extends Marionette.ItemView
+ className: 'modal'
+ template: Templates['coding-rules-bulk-change']
+
+ ui:
+ modalFooter: '.modal-foot'
+ modalError: '.modal-error'
+ modalWarning: '.modal-warning'
+ modalNotice: '.modal-notice'
+ modalField: '.modal-field'
+ codingRulesSubmitBulkChange: '#coding-rules-submit-bulk-change'
+ codingRulesCancelBulkChange: '#coding-rules-cancel-bulk-change'
+ codingRulesCloseBulkChange: '#coding-rules-close-bulk-change'
+
+ events:
+ 'submit form': 'onSubmit'
+ 'click @ui.codingRulesCancelBulkChange': 'hide'
+ 'click @ui.codingRulesCloseBulkChange': 'close'
+ 'change select': 'enableAction'
+
+
+ onRender: ->
+ @$el.dialog
+ dialogClass: 'no-close',
+ width: '600px',
+ draggable: false,
+ autoOpen: false,
+ modal: true,
+ minHeight: 50,
+ resizable: false,
+ title: null
+
+ @$('#coding-rules-bulk-change-profile').select2
+ width: '250px'
+ minimumResultsForSearch: 1
+
+ show: (action, param = null) ->
+ @action = action
+ @profile = param
+ @render()
+ @$el.dialog 'open'
+
+
+ hide: ->
+ @$el.dialog 'close'
+
+
+ close: ->
+ @options.app.fetchFirstPage()
+ @hide()
+
+
+ prepareQuery: ->
+ _.extend @options.app.getQuery(),
+ wsAction: @action
+ profile_key: @$('#coding-rules-bulk-change-profile').val() or @profile
+
+
+ bulkChange: (query) ->
+ wsAction = query.wsAction
+ query = _.omit(query, 'wsAction')
+
+ @ui.modalError.hide()
+ @ui.modalWarning.hide()
+ @ui.modalNotice.hide()
+
+ origFooter = @ui.modalFooter.html()
+ @ui.modalFooter.html '<i class="spinner"></i>'
+
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/qualityprofiles/#{wsAction}_rules"
+ data: query
+ .done (r) =>
+ @ui.modalField.hide()
+ if (r.failed)
+ @ui.modalWarning.show()
+ @ui.modalWarning.html tp('coding_rules.bulk_change.warning', r.succeeded, r.failed)
+ else
+ @ui.modalNotice.show()
+ @ui.modalNotice.html tp('coding_rules.bulk_change.success', r.succeeded)
+
+ @ui.modalFooter.html origFooter
+ @$(@ui.codingRulesSubmitBulkChange.selector).hide()
+ @$(@ui.codingRulesCancelBulkChange.selector).hide()
+ @$(@ui.codingRulesCloseBulkChange.selector).show()
+ .fail =>
+ @ui.modalFooter.html origFooter
+
+
+ onSubmit: (e) ->
+ e.preventDefault()
+ @bulkChange(@prepareQuery())
+
+
+ getAvailableQualityProfiles: ->
+ languages = @options.app.languageFilter.get('value')
+ singleLanguage = _.isArray(languages) && languages.length == 1
+
+ if singleLanguage
+ @options.app.getQualityProfilesForLanguage(languages[0])
+ else
+ @options.app.qualityProfiles
+
+ serializeData: ->
+ action: @action
+
+ paging: @options.app.codingRules.paging
+ qualityProfiles: @options.app.qualityProfiles
+
+ qualityProfile: @profile
+ qualityProfileName: @options.app.qualityProfileFilter.view.renderValue()
+
+ availableQualityProfiles: @getAvailableQualityProfiles()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-custom-rule-creation-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-custom-rule-creation-view.coffee
new file mode 100644
index 00000000000..6274e9fb44c
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-custom-rule-creation-view.coffee
@@ -0,0 +1,175 @@
+define [
+ 'backbone.marionette',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class CodingRulesCustomRuleCreationView extends Marionette.ItemView
+ className: 'modal'
+ template: Templates['coding-rules-custom-rule-creation']
+
+
+ ui:
+ customRuleCreationKey: '#coding-rules-custom-rule-creation-key'
+ customRuleCreationName: '#coding-rules-custom-rule-creation-name'
+ customRuleCreationHtmlDescription: '#coding-rules-custom-rule-creation-html-description'
+ customRuleCreationSeverity: '#coding-rules-custom-rule-creation-severity'
+ customRuleCreationStatus: '#coding-rules-custom-rule-creation-status'
+ customRuleCreationParameters: '[name]'
+ customRuleCreationCreate: '#coding-rules-custom-rule-creation-create'
+ customRuleCreationReactivate: '#coding-rules-custom-rule-creation-reactivate'
+ modalFoot: '.modal-foot'
+
+
+ events:
+ 'input @ui.customRuleCreationName': 'generateKey'
+ 'keydown @ui.customRuleCreationName': 'generateKey'
+ 'keyup @ui.customRuleCreationName': 'generateKey'
+
+ 'input @ui.customRuleCreationKey': 'flagKey'
+ 'keydown @ui.customRuleCreationKey': 'flagKey'
+ 'keyup @ui.customRuleCreationKey': 'flagKey'
+
+ 'click #coding-rules-custom-rule-creation-cancel': 'hide'
+ 'click @ui.customRuleCreationCreate': 'create'
+ 'click @ui.customRuleCreationReactivate': 'reactivate'
+
+
+ generateKey: ->
+ unless @keyModifiedByUser
+ if @ui.customRuleCreationKey
+ generatedKey = @ui.customRuleCreationName.val().latinize().replace(/[^A-Za-z0-9]/g, '_')
+ @ui.customRuleCreationKey.val generatedKey
+
+ flagKey: ->
+ @keyModifiedByUser = true
+ # Cannot use @ui.customRuleCreationReactivate.hide() directly since it was not there at initial render
+ jQuery(@ui.customRuleCreationReactivate.selector).hide()
+
+
+ create: ->
+ action = 'create'
+ if @model and @model.has 'key'
+ action = 'update'
+
+ postData =
+ name: @ui.customRuleCreationName.val()
+ html_description: @ui.customRuleCreationHtmlDescription.val()
+ severity: @ui.customRuleCreationSeverity.val()
+ status: @ui.customRuleCreationStatus.val()
+
+ if @model && @model.has 'key'
+ postData.key = @model.get 'key'
+ else
+ postData.template_key = @templateRule.get 'key'
+ postData.custom_key = @ui.customRuleCreationKey.val()
+ postData.prevent_reactivation = true
+
+ params = @ui.customRuleCreationParameters.map(->
+ key: jQuery(@).prop('name'), value: jQuery(@).val() || jQuery(@).prop('placeholder') || '').get()
+
+ postData.params = (params.map (param) -> param.key + '=' + param.value).join(';')
+ @sendRequest(action, postData)
+
+
+ reactivate: ->
+ postData =
+ name: @existingRule.name
+ html_description: @existingRule.htmlDesc
+ severity: @existingRule.severity
+ status: @existingRule.status
+ template_key: @existingRule.templateKey
+ custom_key: @ui.customRuleCreationKey.val()
+ prevent_reactivation: false
+
+ params = @existingRule.params
+ postData.params = (params.map (param) -> param.key + '=' + param.defaultValue).join(';')
+
+ @sendRequest('create', postData)
+
+
+ sendRequest: (action, postData) ->
+ @$('.modal-error').hide()
+ @$('.modal-warning').hide()
+
+ origFooter = @ui.modalFoot.html()
+ @ui.modalFoot.html '<i class="spinner"></i>'
+
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/rules/" + action
+ data: postData
+ error: () ->
+ .done (r) =>
+ delete @templateRule
+ @options.app.showRule r.rule.key
+ @hide()
+ .fail (jqXHR, textStatus, errorThrown) =>
+ if jqXHR.status == 409
+ @existingRule = jqXHR.responseJSON.rule
+ @$('.modal-warning').show()
+ @ui.modalFoot.html Templates['coding-rules-custom-rule-reactivation'](@)
+ else
+ jQuery.ajaxSettings.error(jqXHR, textStatus, errorThrown)
+ @ui.modalFoot.html origFooter
+
+
+ onRender: ->
+ @$el.dialog
+ dialogClass: 'no-close',
+ width: '600px',
+ draggable: false,
+ autoOpen: false,
+ modal: true,
+ minHeight: 50,
+ resizable: false,
+ title: null
+
+ @keyModifiedByUser = false
+
+ format = (state) ->
+ return state.text unless state.id
+ "<i class='icon-severity-#{state.id.toLowerCase()}'></i> #{state.text}"
+
+ severity = (@model && @model.get 'severity') || @templateRule.get 'severity'
+ @ui.customRuleCreationSeverity.val severity
+ @ui.customRuleCreationSeverity.select2
+ width: '250px'
+ minimumResultsForSearch: 999
+ formatResult: format
+ formatSelection: format
+
+ status = (@model && @model.get 'status') || @templateRule.get 'status'
+ @ui.customRuleCreationStatus.val status
+ @ui.customRuleCreationStatus.select2
+ width: '250px'
+ minimumResultsForSearch: 999
+
+
+ show: ->
+ @render()
+ @$el.dialog 'open'
+
+
+ hide: ->
+ @$el.dialog 'close'
+
+
+ serializeData: ->
+ params = {}
+ if @templateRule
+ params = @templateRule.get 'params'
+ else if @model and @model.has 'params'
+ params = @model.get('params').map (p) ->
+ _.extend p,
+ value: p.defaultValue
+
+ _.extend super,
+ change: @model && @model.has 'key'
+ params: params
+ severities: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']
+ statuses: _.map @options.app.statuses, (value, key) ->
+ id: key
+ text: value
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-custom-rule-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-custom-rule-view.coffee
new file mode 100644
index 00000000000..216704641ef
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-custom-rule-view.coffee
@@ -0,0 +1,42 @@
+define [
+ 'backbone.marionette'
+ 'templates/coding-rules'
+], (
+ Marionette
+ Templates
+) ->
+
+ class CodingRulesDetailCustomRuleView extends Marionette.ItemView
+ tagName: 'tr'
+ className: 'coding-rules-detail-custom-rule'
+ template: Templates['coding-rules-detail-custom-rule']
+
+ ui:
+ delete: '.coding-rules-detail-custom-rule-delete'
+
+ events:
+ 'click @ui.delete': 'delete'
+
+ delete: ->
+ confirmDialog
+ title: t 'delete'
+ html: t 'are_you_sure'
+ yesHandler: =>
+ origEl = @$el.html()
+ @$el.html '<i class="spinner"></i>'
+
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/rules/delete"
+ data:
+ key: @model.get 'key'
+ .done =>
+ templateKey = @options.templateKey or @options.templateRule.get 'key'
+ @options.app.showRule templateKey
+ .fail =>
+ @$el.html origEl
+
+ serializeData: ->
+ _.extend super,
+ templateRule: @options.templateRule
+ canWrite: @options.app.canWrite
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-custom-rules-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-custom-rules-view.coffee
new file mode 100644
index 00000000000..e43d4ca0e93
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-custom-rules-view.coffee
@@ -0,0 +1,16 @@
+define [
+ 'backbone.marionette'
+ 'coding-rules/views/coding-rules-detail-custom-rule-view'
+], (
+ Marionette
+ CodingRulesDetailCustomRuleView
+) ->
+
+ class CodingRulesDetailCustomRulesView extends Marionette.CollectionView
+ tagName: 'table'
+ className: 'width100'
+ itemView: CodingRulesDetailCustomRuleView
+
+ itemViewOptions: ->
+ app: @options.app
+ templateRule: @options.templateRule
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee
new file mode 100644
index 00000000000..7a1544bc9c4
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee
@@ -0,0 +1,101 @@
+define [
+ 'backbone.marionette',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class CodingRulesDetailQualityProfileView extends Marionette.ItemView
+ className: 'coding-rules-detail-quality-profile'
+ template: Templates['coding-rules-detail-quality-profile']
+
+
+ modelEvents:
+ 'change': 'render'
+
+
+ ui:
+ change: '.coding-rules-detail-quality-profile-change'
+ revert: '.coding-rules-detail-quality-profile-revert'
+ deactivate: '.coding-rules-detail-quality-profile-deactivate'
+
+
+ events:
+ 'click @ui.change': 'change'
+ 'click @ui.revert': 'revert'
+ 'click @ui.deactivate': 'deactivate'
+
+
+ change: ->
+ @options.app.codingRulesQualityProfileActivationView.model = @model
+ @options.app.codingRulesQualityProfileActivationView.show()
+
+
+ revert: ->
+ ruleKey = @options.rule.get('key')
+ confirmDialog
+ title: t 'coding_rules.revert_to_parent_definition'
+ html: tp 'coding_rules.revert_to_parent_definition.confirm', @getParent().name
+ yesHandler: =>
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/qualityprofiles/activate_rule"
+ data:
+ profile_key: @model.get('qProfile')
+ rule_key: ruleKey
+ reset: true
+ .done =>
+ @options.app.showRule ruleKey
+
+
+ deactivate: ->
+ ruleKey = @options.rule.get('key')
+ myProfile = _.findWhere(@options.app.qualityProfiles, key: @model.get('qProfile'))
+ confirmDialog
+ title: t 'coding_rules.deactivate'
+ html: tp 'coding_rules.deactivate.confirm', myProfile.name
+ yesHandler: =>
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/qualityprofiles/deactivate_rule"
+ data:
+ profile_key: @model.get('qProfile')
+ rule_key: ruleKey
+ .done =>
+ @options.app.showRule ruleKey
+
+
+ enableUpdate: ->
+ @ui.update.prop 'disabled', false
+
+
+ getParent: ->
+ return null unless @model.get('inherit') && @model.get('inherit') != 'NONE'
+ myProfile = _.findWhere(@options.app.qualityProfiles, key: @model.get('qProfile'))
+ parentKey = myProfile.parentKey
+ parent = _.extend {}, _.findWhere(@options.app.qualityProfiles, key: parentKey)
+ parentActiveInfo = @model.collection.findWhere(qProfile: parentKey) or new Backbone.Model()
+ _.extend parent, parentActiveInfo.toJSON()
+ parent
+
+
+ enhanceParameters: ->
+ parent = @getParent()
+ params = _.sortBy(@model.get('params'), 'key')
+ return params unless parent
+ params.map (p) ->
+ parentParam = _.findWhere(parent.params, key: p.key)
+ if parentParam
+ _.extend p, original: _.findWhere(parent.params, key: p.key).value
+ else
+ p
+
+
+ serializeData: ->
+ hash = _.extend super,
+ parent: @getParent()
+ parameters: @enhanceParameters()
+ canWrite: @options.app.canWrite
+ templateKey: @options.rule.get 'templateKey'
+ isTemplate: @options.rule.get 'isTemplate'
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee
new file mode 100644
index 00000000000..1a5a9f69462
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee
@@ -0,0 +1,15 @@
+define [
+ 'backbone.marionette'
+ 'coding-rules/views/coding-rules-detail-quality-profile-view'
+], (
+ Marionette,
+ CodingRulesDetailQualityProfileView
+) ->
+
+ class CodingRulesDetailQualityProfilesView extends Marionette.CollectionView
+ itemView: CodingRulesDetailQualityProfileView
+
+ itemViewOptions: ->
+ app: @options.app
+ rule: @options.rule
+ qualityProfiles: @collection
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-view.coffee
new file mode 100644
index 00000000000..920dd632376
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-detail-view.coffee
@@ -0,0 +1,305 @@
+define [
+ 'backbone'
+ 'backbone.marionette'
+ 'coding-rules/views/coding-rules-detail-quality-profiles-view'
+ 'coding-rules/views/coding-rules-detail-quality-profile-view'
+ 'coding-rules/views/coding-rules-detail-custom-rules-view'
+ 'coding-rules/views/coding-rules-detail-custom-rule-view'
+ 'templates/coding-rules'
+], (
+ Backbone
+ Marionette
+ CodingRulesDetailQualityProfilesView
+ CodingRulesDetailQualityProfileView
+ CodingRulesDetailCustomRulesView
+ CodingRulesDetailCustomRuleView
+ Templates
+) ->
+
+ class CodingRulesDetailView extends Marionette.Layout
+ template: Templates['coding-rules-detail']
+
+
+ regions:
+ qualityProfilesRegion: '#coding-rules-detail-quality-profiles'
+ customRulesRegion: '.coding-rules-detail-custom-rules-section'
+ customRulesListRegion: '#coding-rules-detail-custom-rules'
+ contextRegion: '.coding-rules-detail-context'
+
+
+ ui:
+ tagsChange: '.coding-rules-detail-tags-change'
+ tagInput: '.coding-rules-detail-tag-input'
+ tagsEdit: '.coding-rules-detail-tag-edit'
+ tagsEditDone: '.coding-rules-detail-tag-edit-done'
+ tagsList: '.coding-rules-detail-tag-list'
+
+ descriptionExtra: '#coding-rules-detail-description-extra'
+ extendDescriptionLink: '#coding-rules-detail-extend-description'
+ extendDescriptionForm: '.coding-rules-detail-extend-description-form'
+ extendDescriptionSubmit: '#coding-rules-detail-extend-description-submit'
+ extendDescriptionRemove: '#coding-rules-detail-extend-description-remove'
+ extendDescriptionText: '#coding-rules-detail-extend-description-text'
+ extendDescriptionSpinner: '#coding-rules-detail-extend-description-spinner'
+ cancelExtendDescription: '#coding-rules-detail-extend-description-cancel'
+
+ activateQualityProfile: '#coding-rules-quality-profile-activate'
+ activateContextQualityProfile: '.coding-rules-detail-quality-profile-activate'
+ changeQualityProfile: '.coding-rules-detail-quality-profile-update'
+ createCustomRule: '#coding-rules-custom-rules-create'
+ changeCustomRule: '#coding-rules-detail-custom-rule-change'
+ deleteCustomRule: '#coding-rules-detail-custom-rule-delete'
+
+
+ events:
+ 'click @ui.tagsChange': 'changeTags'
+ 'click @ui.tagsEditDone': 'editDone'
+
+ 'click @ui.extendDescriptionLink': 'showExtendDescriptionForm'
+ 'click @ui.cancelExtendDescription': 'hideExtendDescriptionForm'
+ 'click @ui.extendDescriptionSubmit': 'submitExtendDescription'
+ 'click @ui.extendDescriptionRemove': 'removeExtendedDescription'
+
+ 'click @ui.activateQualityProfile': 'activateQualityProfile'
+ 'click @ui.activateContextQualityProfile': 'activateContextQualityProfile'
+ 'click @ui.changeQualityProfile': 'changeQualityProfile'
+ 'click @ui.createCustomRule': 'createCustomRule'
+ 'click @ui.changeCustomRule': 'changeCustomRule'
+ 'click @ui.deleteCustomRule': 'deleteCustomRule'
+
+
+ initialize: (options) ->
+ super options
+
+ if @model.get 'params'
+ @model.set 'params', _.sortBy(@model.get('params'), 'key')
+
+ _.map options.actives, (active) =>
+ _.extend active, options.app.getQualityProfileByKey active.qProfile
+ qualityProfiles = new Backbone.Collection options.actives,
+ comparator: 'name'
+ @qualityProfilesView = new CodingRulesDetailQualityProfilesView
+ app: @options.app
+ collection: qualityProfiles
+ rule: @model
+
+ unless @model.get 'isTemplate'
+ qualityProfileKey = @options.app.getQualityProfile()
+
+ if qualityProfileKey
+ @contextProfile = qualityProfiles.findWhere qProfile: qualityProfileKey
+ unless @contextProfile
+ @contextProfile = new Backbone.Model
+ key: qualityProfileKey, name: @options.app.qualityProfileFilter.view.renderValue()
+ @contextQualityProfileView = new CodingRulesDetailQualityProfileView
+ app: @options.app
+ model: @contextProfile
+ rule: @model
+ qualityProfiles: qualityProfiles
+
+ @listenTo @contextProfile, 'destroy', @hideContext
+
+ onRender: ->
+ @$el.find('.open-modal').modal();
+
+ if @model.get 'isTemplate'
+ @$(@contextRegion.el).hide()
+
+ if _.isEmpty(@options.actives)
+ @$(@qualityProfilesRegion.el).hide()
+ else
+ @qualityProfilesRegion.show @qualityProfilesView
+
+ @$(@customRulesRegion.el).show()
+ customRulesOriginal = @$(@customRulesRegion.el).html()
+
+ @$(@customRulesRegion.el).html '<i class="spinner"></i>'
+
+ customRules = new Backbone.Collection()
+ jQuery.ajax
+ url: "#{baseUrl}/api/rules/search"
+ data:
+ template_key: @model.get 'key'
+ f: 'name,severity,params'
+ .done (r) =>
+ customRules.add r.rules
+
+ # Protect against element disappearing due to navigation
+ if @customRulesRegion
+ if customRules.isEmpty() and not @options.app.canWrite
+ @$(@customRulesRegion.el).hide()
+ else
+ @customRulesView = new CodingRulesDetailCustomRulesView
+ app: @options.app
+ collection: customRules
+ templateRule: @model
+ @$(@customRulesRegion.el).html customRulesOriginal
+ @customRulesListRegion.show @customRulesView
+
+ else
+ @$(@customRulesRegion.el).hide()
+ @$(@qualityProfilesRegion.el).show()
+ @qualityProfilesRegion.show @qualityProfilesView
+
+ if @options.app.getQualityProfile()
+ @$(@contextRegion.el).show()
+ @contextRegion.show @contextQualityProfileView
+ else
+ @$(@contextRegion.el).hide()
+
+ that = @
+ jQuery.ajax
+ url: "#{baseUrl}/api/rules/tags"
+ .done (r) =>
+ if @ui.tagInput.select2
+ # Prevent synchronization issue with navigation
+ @ui.tagInput.select2
+ tags: _.difference (_.difference r.tags, that.model.get 'tags'), that.model.get 'sysTags'
+ width: '300px'
+
+ @ui.tagsEdit.hide()
+
+ @ui.extendDescriptionForm.hide()
+ @ui.extendDescriptionSpinner.hide()
+
+
+ hideContext: ->
+ @contextRegion.reset()
+ @$(@contextRegion.el).hide()
+
+
+ changeTags: ->
+ if @ui.tagsEdit.show
+ @ui.tagsEdit.show()
+ if @ui.tagsList.hide
+ @ui.tagsList.hide()
+ key.setScope 'tags'
+ key 'escape', 'tags', => @cancelEdit()
+
+
+ cancelEdit: ->
+ key.unbind 'escape', 'tags'
+ if @ui.tagsList.show
+ @ui.tagsList.show()
+ if @ui.tagInput.select2
+ @ui.tagInput.select2 'close'
+ if @ui.tagsEdit.hide
+ @ui.tagsEdit.hide()
+
+
+ editDone: ->
+ @ui.tagsEdit.html '<i class="spinner"></i>'
+ tags = @ui.tagInput.val()
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/rules/update"
+ data:
+ key: @model.get 'key'
+ tags: tags
+ .done (r) =>
+ @model.set 'tags', r.rule.tags
+ @cancelEdit()
+ .always =>
+ @render()
+
+
+ showExtendDescriptionForm: ->
+ @ui.descriptionExtra.hide()
+ @ui.extendDescriptionForm.show()
+ key.setScope 'extraDesc'
+ key 'escape', 'extraDesc', => @hideExtendDescriptionForm()
+ @ui.extendDescriptionText.focus()
+
+
+ hideExtendDescriptionForm: ->
+ key.unbind 'escape', 'extraDesc'
+ @ui.descriptionExtra.show()
+ @ui.extendDescriptionForm.hide()
+
+
+ submitExtendDescription: ->
+ @ui.extendDescriptionForm.hide()
+ @ui.extendDescriptionSpinner.show()
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/rules/update"
+ dataType: 'json'
+ data:
+ key: @model.get 'key'
+ markdown_note: @ui.extendDescriptionText.val()
+ .done (r) =>
+ @model.set
+ htmlNote: r.rule.htmlNote
+ mdNote: r.rule.mdNote
+ @render()
+
+
+ removeExtendedDescription: ->
+ confirmDialog
+ html: t 'coding_rules.remove_extended_description.confirm'
+ yesHandler: =>
+ @ui.extendDescriptionText.val ''
+ @submitExtendDescription()
+
+
+ activateQualityProfile: ->
+ @options.app.codingRulesQualityProfileActivationView.model = null
+ @options.app.codingRulesQualityProfileActivationView.show()
+
+
+ activateContextQualityProfile: ->
+ @options.app.codingRulesQualityProfileActivationView.model = @contextProfile
+ @options.app.codingRulesQualityProfileActivationView.show()
+
+ createCustomRule: ->
+ @options.app.codingRulesCustomRuleCreationView.templateRule = @model
+ @options.app.codingRulesCustomRuleCreationView.model = new Backbone.Model()
+ @options.app.codingRulesCustomRuleCreationView.show()
+
+
+ changeCustomRule: ->
+ @options.app.codingRulesCustomRuleCreationView.model = @model
+ @options.app.codingRulesCustomRuleCreationView.show()
+
+
+ deleteCustomRule: ->
+ confirmDialog
+ title: t 'delete'
+ html: t 'are_you_sure'
+ yesHandler: =>
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/rules/delete"
+ data:
+ key: @model.get 'key'
+ .done =>
+ @options.app.fetchFirstPage()
+ .fail =>
+ @options.app.showRule @model.get('key')
+
+
+ serializeData: ->
+ contextQualityProfile = @options.app.getQualityProfile()
+ repoKey = @model.get 'repo'
+ isManual = (@options.app.manualRepository().key == repoKey)
+
+ qualityProfilesVisible = not isManual
+ if qualityProfilesVisible
+ if @model.get 'isTemplate'
+ qualityProfilesVisible = (not _.isEmpty(@options.actives))
+ else
+ qualityProfilesVisible = (@options.app.canWrite or not _.isEmpty(@options.actives))
+
+
+ _.extend super,
+ contextQualityProfile: contextQualityProfile
+ contextQualityProfileName: @options.app.qualityProfileFilter.view.renderValue()
+ qualityProfile: @contextProfile
+ language: @options.app.languages[@model.get 'lang']
+ repository: _.find(@options.app.repositories, (repo) -> repo.key == repoKey).name
+ isManual: isManual
+ canWrite: @options.app.canWrite
+ qualityProfilesVisible: qualityProfilesVisible
+ subcharacteristic: (@options.app.characteristics[@model.get 'debtSubChar'] || '').replace ': ', ' > '
+ createdAt: new Date(@model.get 'createdAt')
+ allTags: _.union @model.get('sysTags'), @model.get('tags')
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-facets-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-facets-view.coffee
new file mode 100644
index 00000000000..22204f2929a
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-facets-view.coffee
@@ -0,0 +1,49 @@
+define [
+ 'backbone.marionette'
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class CodingRulesFacetsView extends Marionette.ItemView
+ template: Templates['coding-rules-facets']
+
+
+ ui:
+ facets: '.navigator-facets-list-item'
+ options: '.navigator-facets-list-item-option'
+
+
+ events:
+ 'click @ui.options': 'selectOption'
+
+
+ initialize: ->
+ super()
+ that = @
+ @options.collection.each (facet) ->
+ property = facet.get 'property'
+ facet.set 'property_message', 'coding_rules.facets.' + property
+ _.each(facet.get('values'), (value) ->
+ value.text = that.options.app.facetLabel(property, value.val)
+ )
+
+ selectOption: (e) ->
+ option = jQuery(e.currentTarget)
+ option.toggleClass 'active'
+ @applyOptions()
+
+
+ applyOptions: ->
+ @options.app.fetchFirstPage true
+
+
+ getQuery: ->
+ q = {}
+ if @ui.facets.each
+ @ui.facets.each ->
+ property = jQuery(@).data 'property'
+ activeOptions = jQuery(@).find('.active').map(-> jQuery(@).data 'key').get()
+ q[property] = activeOptions.join ',' if activeOptions.length > 0
+ q
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-empty-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-empty-view.coffee
new file mode 100644
index 00000000000..c3eb8d48c4e
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-empty-view.coffee
@@ -0,0 +1,12 @@
+define [
+ 'backbone.marionette',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class CodingRulesListEmptyView extends Marionette.ItemView
+ tagName: 'li'
+ className: 'navigator-results-no-results'
+ template: Templates['coding-rules-list-empty']
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-item-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-item-view.coffee
new file mode 100644
index 00000000000..b9bf0f82ab0
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-item-view.coffee
@@ -0,0 +1,23 @@
+define [
+ 'backbone.marionette',
+ 'coding-rules/views/coding-rules-detail-view',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ CodingRulesDetailView,
+ Templates
+) ->
+
+ class CodingRulesListItemView extends Marionette.ItemView
+ tagName: 'li'
+ template: Templates['coding-rules-list-item']
+ activeClass: 'active'
+
+
+ events: ->
+ 'click': 'showDetail'
+
+
+ showDetail: ->
+ @options.listView.selectIssue @$el
+ @options.app.showRule @model.get('key')
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-view.coffee
new file mode 100644
index 00000000000..b62e4a28866
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-list-view.coffee
@@ -0,0 +1,93 @@
+define [
+ 'backbone.marionette',
+ 'coding-rules/views/coding-rules-list-item-view',
+ 'coding-rules/views/coding-rules-list-empty-view'
+], (
+ Marionette,
+ CodingRulesListItemView,
+ CodingRulesListEmptyView
+) ->
+
+ class CodingRulesListView extends Marionette.CollectionView
+ tagName: 'ol'
+ className: 'navigator-results-list'
+ itemView: CodingRulesListItemView,
+ emptyView: CodingRulesListEmptyView,
+
+
+ itemViewOptions: ->
+ listView: @, app: @options.app
+
+
+ initialize: ->
+ openRule = (el) -> el.click()
+ @openRule = _.debounce openRule, 300
+ key.setScope 'list'
+
+
+ onRender: ->
+ key 'up', 'list', (e) =>
+ @selectPrev()
+ #e.stopPropagation()
+ key 'down', 'list', (e) =>
+ @selectNext()
+ #e.stopPropagation()
+
+ $scrollEl = jQuery('.navigator-results')
+ scrollEl = $scrollEl.get(0)
+ onScroll = =>
+ if scrollEl.offsetHeight + scrollEl.scrollTop >= scrollEl.scrollHeight
+ @options.app.fetchNextPage()
+ throttledScroll = _.throttle onScroll, 300
+ $scrollEl.off('scroll').on 'scroll', throttledScroll
+
+
+ onClose: ->
+ @unbindEvents()
+
+
+ unbindEvents: ->
+ key.unbind 'up', 'list'
+ key.unbind 'down', 'list'
+
+
+ selectIssue: (el, open) ->
+ @$('.active').removeClass 'active'
+ el.addClass 'active'
+ ruleKey = el.find('[name]').attr('name')
+ rule = @collection.findWhere key: ruleKey
+ @selected = @collection.indexOf(rule)
+ @openRule el if open
+
+
+ selectFirst: ->
+ @selected = -1
+ @selectNext()
+
+
+ selectCurrent: ->
+ @selected--
+ @selectNext()
+
+
+ selectNext: ->
+ if @selected + 1 < @collection.length
+ @selected += 1
+ child = @$el.children().eq(@selected)
+ container = jQuery('.navigator-results')
+ containerHeight = container.height()
+ bottom = child.position().top + child.outerHeight()
+ if bottom > containerHeight
+ container.scrollTop(container.scrollTop() - containerHeight + bottom)
+ @selectIssue child, true
+
+
+ selectPrev: ->
+ if @selected > 0
+ @selected -= 1
+ child = @$el.children().eq(@selected)
+ container = jQuery('.navigator-results')
+ top = child.position().top
+ if top < 0
+ container.scrollTop(container.scrollTop() + top)
+ @selectIssue child, true
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee
new file mode 100644
index 00000000000..fc14043896e
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee
@@ -0,0 +1,124 @@
+define [
+ 'backbone.marionette',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class CodingRulesQualityProfileActivationView extends Marionette.ItemView
+ className: 'modal'
+ template: Templates['coding-rules-quality-profile-activation']
+
+
+ ui:
+ qualityProfileSelect: '#coding-rules-quality-profile-activation-select'
+ qualityProfileSeverity: '#coding-rules-quality-profile-activation-severity'
+ qualityProfileActivate: '#coding-rules-quality-profile-activation-activate'
+ qualityProfileParameters: '[name]'
+
+
+ events:
+ 'click #coding-rules-quality-profile-activation-cancel': 'hide'
+ 'click @ui.qualityProfileActivate': 'activate'
+
+
+ activate: ->
+ profileKey = @ui.qualityProfileSelect.val()
+ params = @ui.qualityProfileParameters.map(->
+ key: jQuery(@).prop('name'), value: jQuery(@).val() || jQuery(@).prop('placeholder') || '').get()
+
+ paramsHash = (params.map (param) -> param.key + '=' + param.value).join(';')
+
+ if @model
+ profileKey = @model.get('qProfile')
+ unless profileKey
+ profileKey = @model.get('key')
+ severity = @ui.qualityProfileSeverity.val()
+
+ origFooter = @$('.modal-foot').html()
+ @$('.modal-foot').html '<i class="spinner"></i>'
+
+ ruleKey = @rule.get('key')
+ jQuery.ajax
+ type: 'POST'
+ url: "#{baseUrl}/api/qualityprofiles/activate_rule"
+ data:
+ profile_key: profileKey
+ rule_key: ruleKey
+ severity: severity
+ params: paramsHash
+ .done =>
+ @options.app.showRule ruleKey
+ @hide()
+ .fail =>
+ @$('.modal-foot').html origFooter
+
+
+ onRender: ->
+ @$el.dialog
+ dialogClass: 'no-close',
+ width: '600px',
+ draggable: false,
+ autoOpen: false,
+ modal: true,
+ minHeight: 50,
+ resizable: false,
+ title: null
+
+ @ui.qualityProfileSelect.select2
+ width: '250px'
+ minimumResultsForSearch: 5
+
+ format = (state) ->
+ return state.text unless state.id
+ "<i class='icon-severity-#{state.id.toLowerCase()}'></i> #{state.text}"
+
+ severity = (@model && @model.get 'severity') || @rule.get 'severity'
+ @ui.qualityProfileSeverity.val severity
+ @ui.qualityProfileSeverity.select2
+ width: '250px'
+ minimumResultsForSearch: 999
+ formatResult: format
+ formatSelection: format
+
+
+ show: ->
+ @render()
+ @$el.dialog 'open'
+
+
+ hide: ->
+ @$el.dialog 'close'
+
+
+ getAvailableQualityProfiles: (lang) ->
+ activeQualityProfiles = @options.app.detailView.qualityProfilesView.collection
+ inactiveProfiles = _.reject @options.app.qualityProfiles, (profile) =>
+ activeQualityProfiles.findWhere key: profile.key
+ _.filter inactiveProfiles, (profile) =>
+ profile.lang == lang
+
+
+ serializeData: ->
+ params = @rule.get 'params'
+ if @model
+ modelParams = @model.get 'params'
+ if modelParams
+ params = params.map (p) ->
+ parentParam = _.findWhere(modelParams, key: p.key)
+ if parentParam
+ _.extend p, value: _.findWhere(modelParams, key: p.key).value
+ else
+ p
+
+ availableProfiles = @getAvailableQualityProfiles(@rule.get 'lang')
+
+ _.extend super,
+ rule: @rule.toJSON()
+ change: @model && @model.has 'severity'
+ params: params
+ qualityProfiles: availableProfiles
+ severities: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']
+ saveEnabled: not _.isEmpty(availableProfiles) or (@model and @model.get('qProfile'))
+ isCustomRule: (@model and @model.has('templateKey')) or @rule.has 'templateKey'
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filter-bar-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filter-bar-view.coffee
new file mode 100644
index 00000000000..84641dd998e
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filter-bar-view.coffee
@@ -0,0 +1,76 @@
+define [
+ 'navigator/filters/filter-bar',
+ 'navigator/filters/base-filters',
+ 'navigator/filters/favorite-filters',
+ 'navigator/filters/more-criteria-filters',
+ 'templates/coding-rules'
+], (
+ FilterBarView,
+ BaseFilters,
+ FavoriteFiltersModule,
+ MoreCriteriaFilters,
+ Templates
+) ->
+
+ class CodingRulesFilterBarView extends FilterBarView
+ template: Templates['coding-rules-filter-bar']
+
+ collectionEvents:
+ 'change:enabled': 'changeEnabled'
+
+
+ events:
+ 'click .navigator-filter-submit': 'search'
+
+
+ onRender: ->
+ @selectFirst()
+
+
+ getQuery: ->
+ query = {}
+ @collection.each (filter) ->
+ _.extend query, filter.view.formatValue()
+ query
+
+
+ onAfterItemAdded: (itemView) ->
+ if itemView.model.get('type') == FavoriteFiltersModule.FavoriteFilterView
+ jQuery('.navigator-header').addClass 'navigator-header-favorite'
+
+
+ addMoreCriteriaFilter: ->
+ disabledFilters = this.collection.where enabled: false
+ if disabledFilters.length > 0
+ @moreCriteriaFilter = new BaseFilters.Filter
+ type: MoreCriteriaFilters.MoreCriteriaFilterView,
+ enabled: true,
+ optional: false,
+ filters: disabledFilters
+ @collection.add @moreCriteriaFilter
+
+
+ changeEnabled: ->
+ if @moreCriteriaFilter?
+ disabledFilters = _.reject @collection.where(enabled: false), (filter) ->
+ filter.get('type') == MoreCriteriaFilters.MoreCriteriaFilterView
+
+ if disabledFilters.length == 0
+ @moreCriteriaFilter.set { enabled: false }, { silent: true }
+ else
+ @moreCriteriaFilter.set { enabled: true }, { silent: true }
+
+ @moreCriteriaFilter.set { filters: disabledFilters }, { silent: true }
+ @moreCriteriaFilter.trigger 'change:filters'
+
+
+ search: ->
+ @$('.navigator-filter-submit').blur()
+ @options.app.state.set
+ query: this.options.app.getQuery(),
+ search: true
+ @options.app.fetchFirstPage()
+
+
+ fetchNextPage: ->
+ @options.app.fetchNextPage()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/activation-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/activation-filter-view.coffee
new file mode 100644
index 00000000000..00f379dc605
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/activation-filter-view.coffee
@@ -0,0 +1,30 @@
+define [
+ 'coding-rules/views/filters/profile-dependent-filter-view'
+], (
+ ProfileDependentFilterView
+) ->
+
+ class ActivationFilterView extends ProfileDependentFilterView
+ tooltip: 'coding_rules.filters.activation.help'
+
+
+ makeActive: ->
+ super
+ unless @model.get 'value'
+ @choices.each (model) -> model.set 'checked', model.id == 'true'
+ @model.set 'value', ['true']
+
+
+
+ showDetails: ->
+ super unless @$el.is '.navigator-filter-inactive'
+
+
+ restore: (value) ->
+ value = value.split(',') if _.isString(value)
+ if @choices && value.length > 0
+ @choices.each (model) -> model.set 'checked', value.indexOf(model.id) >= 0
+ @model.set value: value, enabled: true
+ @onChangeQualityProfile()
+ else
+ @clear()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/active-severities-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/active-severities-filter-view.coffee
new file mode 100644
index 00000000000..3fd1733da6d
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/active-severities-filter-view.coffee
@@ -0,0 +1,7 @@
+define [
+ 'coding-rules/views/filters/profile-dependent-filter-view'
+], (
+ ProfileDependentFilterView
+) ->
+
+ class ActiveSeveritiesFilterView extends ProfileDependentFilterView
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/characteristic-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/characteristic-filter-view.coffee
new file mode 100644
index 00000000000..efa29a733ff
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/characteristic-filter-view.coffee
@@ -0,0 +1,12 @@
+define [
+ 'navigator/filters/choice-filters'
+], (
+ ChoiceFilters
+) ->
+
+ class CharacteriticFilterView extends ChoiceFilters.ChoiceFilterView
+
+ initialize: ->
+ super
+ @choices.comparator = 'text'
+ @choices.sort()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/inheritance-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/inheritance-filter-view.coffee
new file mode 100644
index 00000000000..367fd4210b0
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/inheritance-filter-view.coffee
@@ -0,0 +1,22 @@
+define [
+ 'coding-rules/views/filters/profile-dependent-filter-view'
+], (
+ ProfileDependentFilterView
+) ->
+
+ class InheritanceFilterView extends ProfileDependentFilterView
+
+ onChangeQualityProfile: ->
+ qualityProfileKey = @qualityProfileFilter.get 'value'
+ if _.isArray(qualityProfileKey) && qualityProfileKey.length == 1
+ qualityProfile = @options.app.getQualityProfileByKey qualityProfileKey[0]
+ if qualityProfile.parentKey
+ parentQualityProfile = @options.app.getQualityProfile qualityProfile.parentKey
+ if parentQualityProfile
+ @makeActive()
+ else
+ @makeInactive()
+ else
+ @makeInactive()
+ else
+ @makeInactive()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/language-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/language-filter-view.coffee
new file mode 100644
index 00000000000..b52dcb2bbbb
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/language-filter-view.coffee
@@ -0,0 +1,45 @@
+define [
+ 'navigator/filters/choice-filters',
+ 'templates/coding-rules'
+], (
+ ChoiceFilters,
+ Templates
+) ->
+
+ class LanguageFilterView extends ChoiceFilters.ChoiceFilterView
+
+ modelEvents:
+ 'change:value': 'onChangeValue'
+ 'change:enabled': 'focus',
+
+
+ initialize: ->
+ super
+ @choices.comparator = 'text'
+ @choices.sort()
+ @app = @model.get 'app'
+ @listenTo @app.qualityProfileFilter, 'change:value', @onChangeProfile
+ @selectedFromProfile = false
+
+ onChangeProfile: ->
+ profiles = @app.qualityProfileFilter.get 'value'
+ if _.isArray(profiles) && profiles.length > 0
+ profile = _.findWhere @app.qualityProfiles, key: profiles[0]
+ @options.filterBarView.moreCriteriaFilter.view.detailsView.enableByProperty(@detailsView.model.get 'property')
+ @choices.each (item) -> item.set 'checked', item.id == profile.lang
+ @refreshValues()
+ @selectedFromProfile = true
+ else if @selectedFromProfile
+ @choices.each (item) -> item.set 'checked', false
+ @refreshValues()
+
+ onChangeValue: ->
+ @selectedFromProfile = false
+ @renderBase()
+
+
+ refreshValues: ->
+ @detailsView.updateValue()
+ @detailsView.updateLists()
+ @render()
+ @hideDetails()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/profile-dependent-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/profile-dependent-filter-view.coffee
new file mode 100644
index 00000000000..5afad402b92
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/profile-dependent-filter-view.coffee
@@ -0,0 +1,57 @@
+define [
+ 'navigator/filters/choice-filters'
+], (
+ ChoiceFilters
+) ->
+
+ class ProfileDependentFilterView extends ChoiceFilters.ChoiceFilterView
+ tooltip: 'coding_rules.filters.activation.help'
+
+ initialize: ->
+ super
+ @qualityProfileFilter = @model.get 'qualityProfileFilter'
+ @listenTo @qualityProfileFilter, 'change:value', @onChangeQualityProfile
+ @onChangeQualityProfile()
+
+
+ onChangeQualityProfile: ->
+ qualityProfileKey = @qualityProfileFilter.get 'value'
+ if _.isArray(qualityProfileKey) && qualityProfileKey.length == 1
+ @makeActive()
+ else
+ @makeInactive()
+
+
+ makeActive: ->
+ @model.set inactive: false, title: ''
+ @model.trigger 'change:enabled'
+ @$el.removeClass('navigator-filter-inactive').prop 'title', ''
+ @options.filterBarView.moreCriteriaFilter.view.detailsView.enableByProperty(@detailsView.model.get 'property')
+ @hideDetails()
+
+
+ makeInactive: ->
+ @model.set inactive: true, title: t @tooltip
+ @model.trigger 'change:enabled'
+ @choices.each (model) -> model.set 'checked', false
+ @detailsView.updateLists()
+ @detailsView.updateValue()
+ @$el.addClass('navigator-filter-inactive').prop 'title', t @tooltip
+
+
+ showDetails: ->
+ super unless @$el.is '.navigator-filter-inactive'
+
+
+ restore: (value) ->
+ value = value.split(',') if _.isString(value)
+ if @choices && value.length > 0
+ @model.set value: value, enabled: true
+ @choices.each (item) ->
+ item.set 'checked', false
+ _.each value, (v) =>
+ cModel = @choices.findWhere id: v
+ cModel.set 'checked', true
+ @onChangeQualityProfile()
+ else
+ @clear()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee
new file mode 100644
index 00000000000..d665930cbea
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee
@@ -0,0 +1,61 @@
+define [
+ 'navigator/filters/choice-filters',
+ 'templates/coding-rules'
+], (
+ ChoiceFilters,
+ Templates
+) ->
+
+ class QualityProfileDetailFilterView extends ChoiceFilters.DetailsChoiceFilterView
+ itemTemplate: Templates['coding-rules-profile-filter-detail']
+
+
+ class QualityProfileFilterView extends ChoiceFilters.ChoiceFilterView
+
+ initialize: ->
+ super
+ detailsView: QualityProfileDetailFilterView
+
+ @app = @model.get 'app'
+
+ @allProfiles = @model.get 'choices'
+ @updateChoices @allProfiles
+
+ @listenTo @app.languageFilter, 'change:value', @onChangeLanguage
+ @onChangeLanguage()
+
+
+ onChangeLanguage: ->
+ languages = @app.languageFilter.get 'value'
+ if _.isArray(languages) && languages.length > 0
+ @filterLanguages(languages)
+ else
+ @updateChoices(@allProfiles)
+
+ filterLanguages: (languages) ->
+ languageProfiles = _.filter( @allProfiles, (prof) -> languages.indexOf(prof.lang) >= 0 )
+ @updateChoices(languageProfiles)
+
+
+ updateChoices: (collection) ->
+ languages = @app.languages
+ currentValue = @model.get('value')
+ @choices = new Backbone.Collection( _.map collection, (item, index) ->
+ new Backbone.Model
+ id: item.key
+ text: item.name
+ checked: false
+ index: index
+ language: languages[item.lang]
+ comparator: 'index'
+ )
+ if currentValue
+ @restore(currentValue)
+ @render()
+
+ render: ->
+ super
+ if @model.get 'value'
+ @$el.addClass('navigator-filter-context')
+ else
+ @$el.removeClass('navigator-filter-context')
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/query-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/query-filter-view.coffee
new file mode 100644
index 00000000000..8da995491c2
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/query-filter-view.coffee
@@ -0,0 +1,76 @@
+define [
+ 'backbone',
+ 'backbone.marionette',
+ 'navigator/filters/base-filters',
+ 'navigator/filters/string-filters',
+ 'navigator/filters/choice-filters',
+ 'templates/coding-rules',
+ 'common/handlebars-extensions'
+], (
+ Backbone,
+ Marionette,
+ BaseFilters,
+ StringFilterView,
+ ChoiceFilters,
+ Templates
+) ->
+
+ class QueryFilterView extends StringFilterView
+ template: Templates['coding-rules-query-filter']
+ className: 'navigator-filter navigator-filter-query'
+
+ events:
+ 'keypress input': 'checkSubmit'
+ 'change input': 'change'
+ 'click': 'focus'
+ 'blur': 'blur'
+
+
+ change: (e) ->
+ @model.set 'value', $j(e.target).val()
+ @options.app.codingRules.sorting = sort: '', asc: ''
+
+
+ clear: ->
+ super
+ @focus()
+
+
+ focus: ->
+ @$(':input').focus();
+
+
+ blur: ->
+ @$(':input').blur();
+
+
+ serializeData: ->
+ return _.extend({}, @model.toJSON(),
+ value: this.model.get('value') || ''
+ )
+
+
+ initialize: ->
+ super detailsView: null
+ @model.set('size', 25) unless @model.get 'size'
+
+
+ checkSubmit: (e) ->
+ if (e.which == 13)
+ e.preventDefault()
+ @change(e)
+ @blur()
+ @options.app.filterBarView.$('.navigator-filter-submit').focus()
+ @options.app.filterBarView.$('.navigator-filter-submit').click()
+
+
+ renderInput: ->
+ # Done in template
+
+
+ toggleDetails: ->
+ # NOP
+
+
+ isDefaultValue: ->
+ true
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/repository-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/repository-filter-view.coffee
new file mode 100644
index 00000000000..e37837306f3
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/repository-filter-view.coffee
@@ -0,0 +1,54 @@
+define [
+ 'navigator/filters/choice-filters',
+ 'templates/coding-rules'
+], (
+ ChoiceFilters,
+ Templates
+) ->
+
+ class RepositoryDetailFilterView extends ChoiceFilters.DetailsChoiceFilterView
+ itemTemplate: Templates['coding-rules-repository-detail']
+
+
+ class RepositoryFilterView extends ChoiceFilters.ChoiceFilterView
+
+ initialize: ->
+ super
+ detailsView: RepositoryDetailFilterView
+
+ @app = @model.get 'app'
+
+ @allRepositories = @model.get 'choices'
+ @updateChoices @allRepositories
+
+ @listenTo @app.languageFilter, 'change:value', @onChangeLanguage
+ @onChangeLanguage()
+
+
+ onChangeLanguage: ->
+ languages = @app.languageFilter.get 'value'
+ if _.isArray(languages) && languages.length > 0
+ @filterLanguages(languages)
+ else
+ @updateChoices(@allRepositories)
+
+ filterLanguages: (languages) ->
+ languageRepositories = _.filter( @allRepositories, (repo) -> languages.indexOf(repo.language) >= 0 )
+ @updateChoices(languageRepositories)
+
+
+ updateChoices: (collection) ->
+ languages = @app.languages
+ currentValue = @model.get('value')
+ @choices = new Backbone.Collection( _.map collection, (item, index) ->
+ new Backbone.Model
+ id: item.key
+ text: item.name
+ checked: false
+ index: index
+ language: languages[item.language]
+ comparator: 'index'
+ )
+ if currentValue
+ @restore(currentValue)
+ @render()
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/filters/tag-filter-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/filters/tag-filter-view.coffee
new file mode 100644
index 00000000000..a6b33b8a05e
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/filters/tag-filter-view.coffee
@@ -0,0 +1,37 @@
+define [
+ 'navigator/filters/choice-filters'
+], (
+ ChoiceFilters
+) ->
+
+ class TagFilterView extends ChoiceFilters.ChoiceFilterView
+
+ initialize: ->
+ super()
+ @loadTags()
+ # TODO Register an event handler to reload tags when they are modified on a rule
+
+
+ loadTags: ->
+ tagsXHR = jQuery.ajax
+ url: "#{baseUrl}/api/rules/tags"
+
+ that = @
+ jQuery.when(tagsXHR).done (r) ->
+ that.choices = new Backbone.Collection(
+ _.map(r.tags, (tag) ->
+ new Backbone.Model
+ id: tag
+ text: tag
+ ))
+
+ if that.tagToRestore
+ that.restore(that.tagToRestore)
+ that.tagToRestore = null
+ that.render()
+
+ restore: (value) ->
+ unless @choices.isEmpty()
+ super(value)
+ else
+ @tagToRestore = value
diff --git a/server/sonar-web/src/main/coffee/coding-rules/views/header-view.coffee b/server/sonar-web/src/main/coffee/coding-rules/views/header-view.coffee
new file mode 100644
index 00000000000..9e2ddf78b24
--- /dev/null
+++ b/server/sonar-web/src/main/coffee/coding-rules/views/header-view.coffee
@@ -0,0 +1,18 @@
+define [
+ 'backbone.marionette',
+ 'templates/coding-rules'
+], (
+ Marionette,
+ Templates
+) ->
+
+ class CodingRulesHeaderView extends Marionette.ItemView
+ template: Templates['coding-rules-header']
+
+
+ events:
+ 'click #coding-rules-new-search': 'newSearch'
+
+
+ newSearch: ->
+ @options.app.router.emptyQuery()