diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2014-07-11 17:10:13 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2014-07-11 17:10:13 +0200 |
commit | 1de4e949ef73b086e5ff5cef2b39fdb3c3553d41 (patch) | |
tree | 5f53797f1a21cc7854db1db5f4a4010322e8db45 /server/sonar-web/src/main/coffee/coding-rules | |
parent | d67483d12ef1768adda8f87db349915891f3b19a (diff) | |
download | sonarqube-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')
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() |