From 1c3cbb0af1a1294d2d3c672ee35f10008bee2ac7 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Tue, 25 Mar 2014 15:18:29 +0600 Subject: Move source outside from webapp --- .../src/main/coffee/coding-rules/app.coffee | 343 + .../src/main/coffee/coding-rules/layout.coffee | 29 + .../src/main/coffee/coding-rules/mockjax.coffee | 332 + .../src/main/coffee/coding-rules/router.coffee | 36 + .../coffee/coding-rules/views/actions-view.coffee | 62 + .../coding-rules-bulk-change-dropdown-view.coffee | 51 + .../views/coding-rules-bulk-change-view.coffee | 103 + ...coding-rules-detail-quality-profile-view.coffee | 47 + ...oding-rules-detail-quality-profiles-view.coffee | 14 + .../views/coding-rules-detail-view.coffee | 131 + .../views/coding-rules-list-empty-view.coffee | 12 + .../views/coding-rules-list-item-view.coffee | 34 + .../views/coding-rules-list-view.coffee | 23 + ...ng-rules-quality-profile-activation-view.coffee | 85 + .../coding-rules/views/filter-bar-view.coffee | 71 + .../views/filters/activation-filter-view.coffee | 41 + .../filters/characteristic-filter-view.coffee | 12 + .../views/filters/inheritance-filter-view.coffee | 52 + .../filters/quality-profile-filter-view.coffee | 51 + .../coffee/coding-rules/views/header-view.coffee | 18 + sonar-server/src/main/coffee/common/inputs.coffee | 84 + .../src/main/coffee/quality-gate/app.coffee | 142 + .../quality-gate/collections/conditions.coffee | 11 + .../quality-gate/collections/quality-gates.coffee | 30 + .../src/main/coffee/quality-gate/layout.coffee | 35 + .../coffee/quality-gate/models/condition.coffee | 42 + .../coffee/quality-gate/models/quality-gate.coffee | 10 + .../src/main/coffee/quality-gate/router.coffee | 42 + .../views/quality-gate-actions-view.coffee | 27 + .../quality-gate-detail-condition-view.coffee | 123 + ...uality-gate-detail-conditions-empty-view.coffee | 11 + .../quality-gate-detail-conditions-view.coffee | 86 + .../views/quality-gate-detail-header-view.coffee | 90 + .../views/quality-gate-detail-projects-view.coffee | 38 + .../views/quality-gate-detail-view.coffee | 50 + .../views/quality-gate-edit-view.coffee | 86 + .../quality-gate-sidebar-list-empty-view.coffee | 12 + .../quality-gate-sidebar-list-item-view.coffee | 27 + .../views/quality-gate-sidebar-list-view.coffee | 25 + .../src/main/coffee/tests/common/inputsSpec.coffee | 82 + .../src/main/coffee/tests/translateSpec.coffee | 75 + .../main/hbs/coding-rules/coding-rules-actions.hbs | 18 + .../coding-rules-bulk-change-dropdown.hbs | 15 + .../hbs/coding-rules/coding-rules-bulk-change.hbs | 61 + .../coding-rules-detail-quality-profile.hbs | 54 + .../main/hbs/coding-rules/coding-rules-detail.hbs | 151 + .../hbs/coding-rules/coding-rules-facets-item.hbs | 9 + .../hbs/coding-rules/coding-rules-filter-bar.hbs | 2 + .../main/hbs/coding-rules/coding-rules-header.hbs | 5 + .../main/hbs/coding-rules/coding-rules-layout.hbs | 5 + .../hbs/coding-rules/coding-rules-list-empty.hbs | 1 + .../hbs/coding-rules/coding-rules-list-item.hbs | 5 + .../coding-rules-quality-profile-activation.hbs | 59 + .../src/main/hbs/common/_markdown-tips.hbs | 4 + .../hbs/quality-gates/quality-gate-actions.hbs | 6 + .../quality-gate-detail-condition.hbs | 59 + .../quality-gate-detail-conditions-empty.hbs | 3 + .../quality-gate-detail-conditions.hbs | 43 + .../quality-gates/quality-gate-detail-header.hbs | 14 + .../quality-gates/quality-gate-detail-projects.hbs | 13 + .../main/hbs/quality-gates/quality-gate-detail.hbs | 2 + .../main/hbs/quality-gates/quality-gate-edit.hbs | 22 + .../quality-gate-sidebar-list-empty.hbs | 1 + .../quality-gate-sidebar-list-item.hbs | 1 + .../hbs/quality-gates/quality-gates-layout.hbs | 4 + sonar-server/src/main/js/application.js | 556 + .../src/main/js/common/handlebars-extensions.js | 163 + sonar-server/src/main/js/common/select-list.js | 428 + sonar-server/src/main/js/dashboard.js | 134 + sonar-server/src/main/js/duplication.js | 21 + sonar-server/src/main/js/issue.js | 250 + sonar-server/src/main/js/issues/app.js | 402 + sonar-server/src/main/js/issues/extra.js | 1242 +++ sonar-server/src/main/js/measures/app.js | 195 + .../js/navigator/filters/action-plan-filters.js | 104 + .../js/navigator/filters/ajax-select-filters.js | 465 + .../src/main/js/navigator/filters/base-filters.js | 225 + .../main/js/navigator/filters/checkbox-filters.js | 50 + .../main/js/navigator/filters/choice-filters.js | 363 + .../main/js/navigator/filters/context-filters.js | 66 + .../js/navigator/filters/date-filter-view.coffee | 11 + .../main/js/navigator/filters/date-filter-view.js | 24 + .../main/js/navigator/filters/favorite-filters.js | 79 + .../src/main/js/navigator/filters/filter-bar.js | 107 + .../main/js/navigator/filters/metric-filters.js | 169 + .../js/navigator/filters/more-criteria-filters.js | 70 + .../src/main/js/navigator/filters/range-filters.js | 195 + .../main/js/navigator/filters/read-only-filters.js | 25 + .../src/main/js/navigator/filters/rule-filters.js | 50 + .../main/js/navigator/filters/string-filters.js | 77 + sonar-server/src/main/js/recent-history.js | 65 + sonar-server/src/main/js/require.js | 36 + sonar-server/src/main/js/resource.js | 59 + sonar-server/src/main/js/select2-jquery-ui-fix.js | 9 + sonar-server/src/main/js/sortable.js | 74 + sonar-server/src/main/js/tests/main.js | 45 + .../tests/navigator/filters/BaseFilterViewSpec.js | 15 + .../navigator/filters/ChoiceFilterViewSpec.js | 39 + sonar-server/src/main/js/third-party/backbone.js | 1581 +++ .../src/main/js/third-party/backbone.marionette.js | 2553 +++++ sonar-server/src/main/js/third-party/d3.js | 9275 +++++++++++++++++ sonar-server/src/main/js/third-party/handlebars.js | 2610 +++++ sonar-server/src/main/js/third-party/jquery-ui.js | 5399 ++++++++++ sonar-server/src/main/js/third-party/jquery.js | 10337 +++++++++++++++++++ .../src/main/js/third-party/jquery.mockjax.js | 598 ++ sonar-server/src/main/js/third-party/moment.js | 2428 +++++ sonar-server/src/main/js/third-party/select2.js | 2407 +++++ sonar-server/src/main/js/third-party/underscore.js | 1344 +++ sonar-server/src/main/js/top-search.js | 184 + sonar-server/src/main/js/translate.js | 46 + sonar-server/src/main/js/widgets/bubble-chart.js | 496 + sonar-server/src/main/js/widgets/histogram.js | 304 + sonar-server/src/main/js/widgets/pie-chart.js | 393 + sonar-server/src/main/js/widgets/stack-area.js | 371 + sonar-server/src/main/js/widgets/timeline.js | 416 + sonar-server/src/main/js/widgets/widget.js | 85 + sonar-server/src/main/less/coding-rules.less | 230 + sonar-server/src/main/less/dashboard.less | 265 + sonar-server/src/main/less/icons.less | 340 + sonar-server/src/main/less/jquery-ui.less | 368 + sonar-server/src/main/less/layout.less | 304 + sonar-server/src/main/less/mixins.less | 171 + sonar-server/src/main/less/navigator.less | 6 + sonar-server/src/main/less/navigator/base.less | 524 + sonar-server/src/main/less/navigator/config.less | 25 + sonar-server/src/main/less/navigator/filters.less | 292 + sonar-server/src/main/less/quality-gates.less | 116 + sonar-server/src/main/less/select-list.less | 189 + sonar-server/src/main/less/select2-sonar.less | 183 + sonar-server/src/main/less/select2.less | 536 + sonar-server/src/main/less/sonar-colorizer.less | 61 + sonar-server/src/main/less/style.less | 2757 +++++ sonar-server/src/main/less/ui.less | 129 + sonar-server/src/main/less/variables.less | 62 + sonar-server/src/main/less/yui-reset-font.less | 12 + .../src/main/webapp/coffee/coding-rules/app.coffee | 343 - .../main/webapp/coffee/coding-rules/layout.coffee | 29 - .../main/webapp/coffee/coding-rules/mockjax.coffee | 332 - .../main/webapp/coffee/coding-rules/router.coffee | 36 - .../coffee/coding-rules/views/actions-view.coffee | 62 - .../coding-rules-bulk-change-dropdown-view.coffee | 51 - .../views/coding-rules-bulk-change-view.coffee | 103 - ...coding-rules-detail-quality-profile-view.coffee | 47 - ...oding-rules-detail-quality-profiles-view.coffee | 14 - .../views/coding-rules-detail-view.coffee | 131 - .../views/coding-rules-list-empty-view.coffee | 12 - .../views/coding-rules-list-item-view.coffee | 34 - .../views/coding-rules-list-view.coffee | 23 - ...ng-rules-quality-profile-activation-view.coffee | 85 - .../coding-rules/views/filter-bar-view.coffee | 71 - .../views/filters/activation-filter-view.coffee | 41 - .../filters/characteristic-filter-view.coffee | 12 - .../views/filters/inheritance-filter-view.coffee | 52 - .../filters/quality-profile-filter-view.coffee | 51 - .../coffee/coding-rules/views/header-view.coffee | 18 - .../src/main/webapp/coffee/quality-gate/app.coffee | 142 - .../quality-gate/collections/conditions.coffee | 11 - .../quality-gate/collections/quality-gates.coffee | 30 - .../main/webapp/coffee/quality-gate/layout.coffee | 35 - .../coffee/quality-gate/models/condition.coffee | 42 - .../coffee/quality-gate/models/quality-gate.coffee | 10 - .../main/webapp/coffee/quality-gate/router.coffee | 42 - .../views/quality-gate-actions-view.coffee | 27 - .../quality-gate-detail-condition-view.coffee | 123 - ...uality-gate-detail-conditions-empty-view.coffee | 11 - .../quality-gate-detail-conditions-view.coffee | 86 - .../views/quality-gate-detail-header-view.coffee | 90 - .../views/quality-gate-detail-projects-view.coffee | 38 - .../views/quality-gate-detail-view.coffee | 50 - .../views/quality-gate-edit-view.coffee | 86 - .../quality-gate-sidebar-list-empty-view.coffee | 12 - .../quality-gate-sidebar-list-item-view.coffee | 27 - .../views/quality-gate-sidebar-list-view.coffee | 25 - .../webapp/coffee/tests/common/inputsSpec.coffee | 82 - .../main/webapp/coffee/tests/translateSpec.coffee | 75 - sonar-server/src/main/webapp/js/application.js | 556 - sonar-server/src/main/webapp/js/build.js | 41 - .../main/webapp/js/common/handlebars-extensions.js | 163 - .../src/main/webapp/js/common/inputs.coffee | 84 - sonar-server/src/main/webapp/js/common/inputs.js | 108 - .../src/main/webapp/js/common/select-list.js | 428 - sonar-server/src/main/webapp/js/dashboard.js | 134 - sonar-server/src/main/webapp/js/duplication.js | 21 - sonar-server/src/main/webapp/js/issue.js | 250 - sonar-server/src/main/webapp/js/issues/app.js | 402 - sonar-server/src/main/webapp/js/issues/extra.js | 1242 --- sonar-server/src/main/webapp/js/measures/app.js | 195 - .../js/navigator/filters/action-plan-filters.js | 104 - .../js/navigator/filters/ajax-select-filters.js | 465 - .../webapp/js/navigator/filters/base-filters.js | 225 - .../js/navigator/filters/checkbox-filters.js | 50 - .../webapp/js/navigator/filters/choice-filters.js | 363 - .../webapp/js/navigator/filters/context-filters.js | 66 - .../js/navigator/filters/date-filter-view.coffee | 11 - .../js/navigator/filters/date-filter-view.js | 24 - .../js/navigator/filters/favorite-filters.js | 79 - .../main/webapp/js/navigator/filters/filter-bar.js | 107 - .../webapp/js/navigator/filters/metric-filters.js | 169 - .../js/navigator/filters/more-criteria-filters.js | 70 - .../webapp/js/navigator/filters/range-filters.js | 195 - .../js/navigator/filters/read-only-filters.js | 25 - .../webapp/js/navigator/filters/rule-filters.js | 50 - .../webapp/js/navigator/filters/string-filters.js | 77 - sonar-server/src/main/webapp/js/recent-history.js | 65 - sonar-server/src/main/webapp/js/require.js | 36 - sonar-server/src/main/webapp/js/resource.js | 59 - .../src/main/webapp/js/select2-jquery-ui-fix.js | 9 - sonar-server/src/main/webapp/js/sortable.js | 74 - sonar-server/src/main/webapp/js/tests/main.js | 45 - .../tests/navigator/filters/BaseFilterViewSpec.js | 15 - .../navigator/filters/ChoiceFilterViewSpec.js | 39 - .../src/main/webapp/js/third-party/backbone.js | 1581 --- .../webapp/js/third-party/backbone.marionette.js | 2553 ----- sonar-server/src/main/webapp/js/third-party/d3.js | 9275 ----------------- .../src/main/webapp/js/third-party/handlebars.js | 2610 ----- .../src/main/webapp/js/third-party/jquery-ui.js | 5399 ---------- .../src/main/webapp/js/third-party/jquery.js | 10337 ------------------- .../main/webapp/js/third-party/jquery.mockjax.js | 598 -- .../src/main/webapp/js/third-party/moment.js | 2428 ----- .../src/main/webapp/js/third-party/select2.js | 2407 ----- .../src/main/webapp/js/third-party/underscore.js | 1344 --- sonar-server/src/main/webapp/js/top-search.js | 184 - sonar-server/src/main/webapp/js/translate.js | 46 - .../src/main/webapp/js/widgets/bubble-chart.js | 496 - .../src/main/webapp/js/widgets/histogram.js | 304 - .../src/main/webapp/js/widgets/pie-chart.js | 393 - .../src/main/webapp/js/widgets/stack-area.js | 371 - .../src/main/webapp/js/widgets/timeline.js | 416 - sonar-server/src/main/webapp/js/widgets/widget.js | 85 - .../src/main/webapp/less/coding-rules.less | 230 - sonar-server/src/main/webapp/less/dashboard.less | 265 - sonar-server/src/main/webapp/less/icons.less | 340 - sonar-server/src/main/webapp/less/jquery-ui.less | 368 - sonar-server/src/main/webapp/less/layout.less | 304 - sonar-server/src/main/webapp/less/mixins.less | 171 - sonar-server/src/main/webapp/less/navigator.less | 6 - .../src/main/webapp/less/navigator/base.less | 524 - .../src/main/webapp/less/navigator/config.less | 25 - .../src/main/webapp/less/navigator/filters.less | 292 - .../src/main/webapp/less/quality-gates.less | 116 - sonar-server/src/main/webapp/less/select-list.less | 189 - .../src/main/webapp/less/select2-sonar.less | 183 - sonar-server/src/main/webapp/less/select2.less | 536 - .../src/main/webapp/less/sonar-colorizer.less | 61 - sonar-server/src/main/webapp/less/style.less | 2757 ----- sonar-server/src/main/webapp/less/ui.less | 129 - sonar-server/src/main/webapp/less/variables.less | 62 - .../src/main/webapp/less/yui-reset-font.less | 12 - .../coding-rules/coding-rules-actions.hbs | 18 - .../coding-rules-bulk-change-dropdown.hbs | 15 - .../coding-rules/coding-rules-bulk-change.hbs | 61 - .../coding-rules-detail-quality-profile.hbs | 54 - .../templates/coding-rules/coding-rules-detail.hbs | 151 - .../coding-rules/coding-rules-facets-item.hbs | 9 - .../coding-rules/coding-rules-filter-bar.hbs | 2 - .../templates/coding-rules/coding-rules-header.hbs | 5 - .../templates/coding-rules/coding-rules-layout.hbs | 5 - .../coding-rules/coding-rules-list-empty.hbs | 1 - .../coding-rules/coding-rules-list-item.hbs | 5 - .../coding-rules-quality-profile-activation.hbs | 59 - .../webapp/templates/common/_markdown-tips.hbs | 4 - .../quality-gates/quality-gate-actions.hbs | 6 - .../quality-gate-detail-condition.hbs | 59 - .../quality-gate-detail-conditions-empty.hbs | 3 - .../quality-gate-detail-conditions.hbs | 43 - .../quality-gates/quality-gate-detail-header.hbs | 14 - .../quality-gates/quality-gate-detail-projects.hbs | 13 - .../quality-gates/quality-gate-detail.hbs | 2 - .../templates/quality-gates/quality-gate-edit.hbs | 20 - .../quality-gate-sidebar-list-empty.hbs | 1 - .../quality-gate-sidebar-list-item.hbs | 1 - .../quality-gates/quality-gates-layout.hbs | 4 - 272 files changed, 56442 insertions(+), 56589 deletions(-) create mode 100644 sonar-server/src/main/coffee/coding-rules/app.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/layout.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/mockjax.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/router.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/actions-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-empty-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-item-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/filter-bar-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/filters/activation-filter-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/filters/characteristic-filter-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/filters/inheritance-filter-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee create mode 100644 sonar-server/src/main/coffee/coding-rules/views/header-view.coffee create mode 100644 sonar-server/src/main/coffee/common/inputs.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/app.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/collections/conditions.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/collections/quality-gates.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/layout.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/models/condition.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/models/quality-gate.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/router.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-actions-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-header-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-edit-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee create mode 100644 sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee create mode 100644 sonar-server/src/main/coffee/tests/common/inputsSpec.coffee create mode 100644 sonar-server/src/main/coffee/tests/translateSpec.coffee create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-actions.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-bulk-change-dropdown.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-bulk-change.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-detail-quality-profile.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-detail.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-facets-item.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-filter-bar.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-header.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-layout.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-list-empty.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-list-item.hbs create mode 100644 sonar-server/src/main/hbs/coding-rules/coding-rules-quality-profile-activation.hbs create mode 100644 sonar-server/src/main/hbs/common/_markdown-tips.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-actions.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-detail-condition.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-detail-conditions-empty.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-detail-conditions.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-detail-header.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-detail-projects.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-detail.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-edit.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-sidebar-list-empty.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gate-sidebar-list-item.hbs create mode 100644 sonar-server/src/main/hbs/quality-gates/quality-gates-layout.hbs create mode 100644 sonar-server/src/main/js/application.js create mode 100644 sonar-server/src/main/js/common/handlebars-extensions.js create mode 100644 sonar-server/src/main/js/common/select-list.js create mode 100644 sonar-server/src/main/js/dashboard.js create mode 100644 sonar-server/src/main/js/duplication.js create mode 100644 sonar-server/src/main/js/issue.js create mode 100644 sonar-server/src/main/js/issues/app.js create mode 100644 sonar-server/src/main/js/issues/extra.js create mode 100644 sonar-server/src/main/js/measures/app.js create mode 100644 sonar-server/src/main/js/navigator/filters/action-plan-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/ajax-select-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/base-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/checkbox-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/choice-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/context-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/date-filter-view.coffee create mode 100644 sonar-server/src/main/js/navigator/filters/date-filter-view.js create mode 100644 sonar-server/src/main/js/navigator/filters/favorite-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/filter-bar.js create mode 100644 sonar-server/src/main/js/navigator/filters/metric-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/more-criteria-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/range-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/read-only-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/rule-filters.js create mode 100644 sonar-server/src/main/js/navigator/filters/string-filters.js create mode 100644 sonar-server/src/main/js/recent-history.js create mode 100644 sonar-server/src/main/js/require.js create mode 100644 sonar-server/src/main/js/resource.js create mode 100644 sonar-server/src/main/js/select2-jquery-ui-fix.js create mode 100644 sonar-server/src/main/js/sortable.js create mode 100644 sonar-server/src/main/js/tests/main.js create mode 100644 sonar-server/src/main/js/tests/navigator/filters/BaseFilterViewSpec.js create mode 100644 sonar-server/src/main/js/tests/navigator/filters/ChoiceFilterViewSpec.js create mode 100644 sonar-server/src/main/js/third-party/backbone.js create mode 100644 sonar-server/src/main/js/third-party/backbone.marionette.js create mode 100644 sonar-server/src/main/js/third-party/d3.js create mode 100644 sonar-server/src/main/js/third-party/handlebars.js create mode 100755 sonar-server/src/main/js/third-party/jquery-ui.js create mode 100644 sonar-server/src/main/js/third-party/jquery.js create mode 100644 sonar-server/src/main/js/third-party/jquery.mockjax.js create mode 100644 sonar-server/src/main/js/third-party/moment.js create mode 100644 sonar-server/src/main/js/third-party/select2.js create mode 100644 sonar-server/src/main/js/third-party/underscore.js create mode 100644 sonar-server/src/main/js/top-search.js create mode 100644 sonar-server/src/main/js/translate.js create mode 100644 sonar-server/src/main/js/widgets/bubble-chart.js create mode 100644 sonar-server/src/main/js/widgets/histogram.js create mode 100644 sonar-server/src/main/js/widgets/pie-chart.js create mode 100644 sonar-server/src/main/js/widgets/stack-area.js create mode 100644 sonar-server/src/main/js/widgets/timeline.js create mode 100644 sonar-server/src/main/js/widgets/widget.js create mode 100644 sonar-server/src/main/less/coding-rules.less create mode 100644 sonar-server/src/main/less/dashboard.less create mode 100644 sonar-server/src/main/less/icons.less create mode 100644 sonar-server/src/main/less/jquery-ui.less create mode 100644 sonar-server/src/main/less/layout.less create mode 100644 sonar-server/src/main/less/mixins.less create mode 100644 sonar-server/src/main/less/navigator.less create mode 100644 sonar-server/src/main/less/navigator/base.less create mode 100644 sonar-server/src/main/less/navigator/config.less create mode 100644 sonar-server/src/main/less/navigator/filters.less create mode 100644 sonar-server/src/main/less/quality-gates.less create mode 100644 sonar-server/src/main/less/select-list.less create mode 100644 sonar-server/src/main/less/select2-sonar.less create mode 100755 sonar-server/src/main/less/select2.less create mode 100644 sonar-server/src/main/less/sonar-colorizer.less create mode 100644 sonar-server/src/main/less/style.less create mode 100644 sonar-server/src/main/less/ui.less create mode 100644 sonar-server/src/main/less/variables.less create mode 100644 sonar-server/src/main/less/yui-reset-font.less delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/app.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/layout.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/mockjax.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/router.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/actions-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-detail-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-list-empty-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-list-item-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-list-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/filter-bar-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/filters/activation-filter-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/filters/characteristic-filter-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/filters/inheritance-filter-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/coding-rules/views/header-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/app.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/collections/conditions.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/collections/quality-gates.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/layout.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/models/condition.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/models/quality-gate.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/router.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-actions-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-detail-header-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-detail-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-edit-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/tests/common/inputsSpec.coffee delete mode 100644 sonar-server/src/main/webapp/coffee/tests/translateSpec.coffee delete mode 100644 sonar-server/src/main/webapp/js/application.js delete mode 100644 sonar-server/src/main/webapp/js/build.js delete mode 100644 sonar-server/src/main/webapp/js/common/handlebars-extensions.js delete mode 100644 sonar-server/src/main/webapp/js/common/inputs.coffee delete mode 100644 sonar-server/src/main/webapp/js/common/inputs.js delete mode 100644 sonar-server/src/main/webapp/js/common/select-list.js delete mode 100644 sonar-server/src/main/webapp/js/dashboard.js delete mode 100644 sonar-server/src/main/webapp/js/duplication.js delete mode 100644 sonar-server/src/main/webapp/js/issue.js delete mode 100644 sonar-server/src/main/webapp/js/issues/app.js delete mode 100644 sonar-server/src/main/webapp/js/issues/extra.js delete mode 100644 sonar-server/src/main/webapp/js/measures/app.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/action-plan-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/ajax-select-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/base-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/checkbox-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/choice-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/context-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/date-filter-view.coffee delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/date-filter-view.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/favorite-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/filter-bar.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/metric-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/more-criteria-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/range-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/read-only-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/rule-filters.js delete mode 100644 sonar-server/src/main/webapp/js/navigator/filters/string-filters.js delete mode 100644 sonar-server/src/main/webapp/js/recent-history.js delete mode 100644 sonar-server/src/main/webapp/js/require.js delete mode 100644 sonar-server/src/main/webapp/js/resource.js delete mode 100644 sonar-server/src/main/webapp/js/select2-jquery-ui-fix.js delete mode 100644 sonar-server/src/main/webapp/js/sortable.js delete mode 100644 sonar-server/src/main/webapp/js/tests/main.js delete mode 100644 sonar-server/src/main/webapp/js/tests/navigator/filters/BaseFilterViewSpec.js delete mode 100644 sonar-server/src/main/webapp/js/tests/navigator/filters/ChoiceFilterViewSpec.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/backbone.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/backbone.marionette.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/d3.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/handlebars.js delete mode 100755 sonar-server/src/main/webapp/js/third-party/jquery-ui.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/jquery.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/jquery.mockjax.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/moment.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/select2.js delete mode 100644 sonar-server/src/main/webapp/js/third-party/underscore.js delete mode 100644 sonar-server/src/main/webapp/js/top-search.js delete mode 100644 sonar-server/src/main/webapp/js/translate.js delete mode 100644 sonar-server/src/main/webapp/js/widgets/bubble-chart.js delete mode 100644 sonar-server/src/main/webapp/js/widgets/histogram.js delete mode 100644 sonar-server/src/main/webapp/js/widgets/pie-chart.js delete mode 100644 sonar-server/src/main/webapp/js/widgets/stack-area.js delete mode 100644 sonar-server/src/main/webapp/js/widgets/timeline.js delete mode 100644 sonar-server/src/main/webapp/js/widgets/widget.js delete mode 100644 sonar-server/src/main/webapp/less/coding-rules.less delete mode 100644 sonar-server/src/main/webapp/less/dashboard.less delete mode 100644 sonar-server/src/main/webapp/less/icons.less delete mode 100644 sonar-server/src/main/webapp/less/jquery-ui.less delete mode 100644 sonar-server/src/main/webapp/less/layout.less delete mode 100644 sonar-server/src/main/webapp/less/mixins.less delete mode 100644 sonar-server/src/main/webapp/less/navigator.less delete mode 100644 sonar-server/src/main/webapp/less/navigator/base.less delete mode 100644 sonar-server/src/main/webapp/less/navigator/config.less delete mode 100644 sonar-server/src/main/webapp/less/navigator/filters.less delete mode 100644 sonar-server/src/main/webapp/less/quality-gates.less delete mode 100644 sonar-server/src/main/webapp/less/select-list.less delete mode 100644 sonar-server/src/main/webapp/less/select2-sonar.less delete mode 100755 sonar-server/src/main/webapp/less/select2.less delete mode 100644 sonar-server/src/main/webapp/less/sonar-colorizer.less delete mode 100644 sonar-server/src/main/webapp/less/style.less delete mode 100644 sonar-server/src/main/webapp/less/ui.less delete mode 100644 sonar-server/src/main/webapp/less/variables.less delete mode 100644 sonar-server/src/main/webapp/less/yui-reset-font.less delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-actions.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-bulk-change-dropdown.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-bulk-change.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-detail-quality-profile.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-detail.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-facets-item.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-filter-bar.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-header.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-layout.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-list-empty.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-list-item.hbs delete mode 100644 sonar-server/src/main/webapp/templates/coding-rules/coding-rules-quality-profile-activation.hbs delete mode 100644 sonar-server/src/main/webapp/templates/common/_markdown-tips.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-actions.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-detail-condition.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-detail-conditions-empty.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-detail-conditions.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-detail-header.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-detail-projects.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-detail.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-edit.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-sidebar-list-empty.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gate-sidebar-list-item.hbs delete mode 100644 sonar-server/src/main/webapp/templates/quality-gates/quality-gates-layout.hbs (limited to 'sonar-server/src/main') diff --git a/sonar-server/src/main/coffee/coding-rules/app.coffee b/sonar-server/src/main/coffee/coding-rules/app.coffee new file mode 100644 index 00000000000..b5029b0f577 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/app.coffee @@ -0,0 +1,343 @@ +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-bulk-change-view', + 'coding-rules/views/coding-rules-quality-profile-activation-view', + 'coding-rules/views/coding-rules-bulk-change-dropdown-view' + + # filters + 'navigator/filters/base-filters', + 'navigator/filters/choice-filters', + 'navigator/filters/string-filters', + 'navigator/filters/date-filter-view', + 'coding-rules/views/filters/quality-profile-filter-view', + 'coding-rules/views/filters/inheritance-filter-view', + 'coding-rules/views/filters/activation-filter-view', + 'coding-rules/views/filters/characteristic-filter-view', + + 'coding-rules/mockjax', + 'common/handlebars-extensions' +], ( + Backbone, Marionette, + + CodingRulesLayout, + CodingRulesRouter, + + # views + CodingRulesHeaderView, + CodingRulesActionsView, + CodingRulesFilterBarView, + CodingRulesListView, + CodingRulesBulkChangeView, + CodingRulesQualityProfileActivationView, + CodingRulesBulkChangeDropdownView + + # filters + BaseFilters, + ChoiceFilters, + StringFilterView, + DateFilterView, + QualityProfileFilterView, + InheritanceFilterView + ActivationFilterView + CharacteristicFilterView +) -> + + # 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 '. ' + 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 = -> + @filterBarView.getQuery() + + + App.restoreSorting = -> + + + + App.storeQuery = (query, sorting) -> + if sorting + _.extend query, + sort: sorting.sort + asc: '' + sorting.asc + queryString = _.map query, (v, k) -> "#{k}=#{encodeURIComponent(v)}" + @router.navigate queryString.join('|'), replace: true + + + + App.fetchList = (firstPage) -> + query = @getQuery() + fetchQuery = _.extend { pageIndex: @pageIndex }, query + + if @codingRules.sorting + _.extend fetchQuery, + sort: @codingRules.sorting.sort, + asc: @codingRules.sorting.asc + + @storeQuery query, @codingRules.sorting + + @layout.showSpinner 'resultsRegion' + jQuery.ajax + url: "#{baseUrl}/api/codingrules/search" + data: fetchQuery + .done (r) => + if firstPage + @codingRules.reset r.codingrules + else + @codingRules.add r.codingrules + @codingRules.paging = r.paging + @codingRulesListView = new CodingRulesListView + app: @ + collection: @codingRules + @layout.resultsRegion.show @codingRulesListView + @codingRulesListView.selectFirst() + + + + App.fetchFirstPage = -> + @pageIndex = 1 + App.fetchList true + + + App.fetchNextPage = -> + if @pageIndex < @codingRules.paging.pages + @pageIndex++ + App.fetchList false + + + App.getQualityProfile = -> + value = @qualityProfileFilter.get('value') + if value? && value.length == 1 then value[0] else null + + + # Construct layout + App.addInitializer -> + @layout = new CodingRulesLayout app: @ + jQuery('body').append @layout.render().el + + + # Construct header + App.addInitializer -> + @codingRulesHeaderView = new CodingRulesHeaderView app: @ + @layout.headerRegion.show @codingRulesHeaderView + + + # Define coding rules + App.addInitializer -> + @codingRules = new Backbone.Collection + @codingRules.sorting = sort: 'CREATION_DATE', asc: false + + + # 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: @ + + + # Define filters + App.addInitializer -> + @filters = new BaseFilters.Filters + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.name' + property: 'name' + type: StringFilterView + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.language' + property: 'languages' + type: ChoiceFilters.ChoiceFilterView + choices: @languages + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.severity' + property: 'severities' + type: ChoiceFilters.ChoiceFilterView + 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: ChoiceFilters.ChoiceFilterView + choices: @tags + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.characteristic' + property: 'characteristic' + type: CharacteristicFilterView + choices: @characteristics + multiple: false + + @qualityProfileFilter = new BaseFilters.Filter + name: t 'coding_rules.filters.quality_profile' + property: 'quality_profile' + type: QualityProfileFilterView + multiple: false + @filters.add @qualityProfileFilter + + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.activation' + property: 'activation' + type: ActivationFilterView + enabled: false + optional: false + multiple: false + qualityProfileFilter: @qualityProfileFilter + choices: + 'active': t 'coding_rules.filters.activation.active' + 'inactive': t 'coding_rules.filters.activation.inactive' + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.availableSince' + property: 'availableSince' + type: DateFilterView + enabled: false + optional: true + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.description' + property: 'description' + type: StringFilterView + 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: + 'not_inhertited': t 'coding_rules.filters.inheritance.not_inherited' + 'inhertited': t 'coding_rules.filters.inheritance.inherited' + 'overriden': t 'coding_rules.filters.inheritance.overriden' + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.key' + property: 'key' + type: StringFilterView + enabled: false + optional: true + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.repository' + property: 'repositories' + type: ChoiceFilters.ChoiceFilterView + enabled: false + optional: true + choices: @repositories + + @filters.add new BaseFilters.Filter + name: t 'coding_rules.filters.status' + property: 'statuses' + type: ChoiceFilters.ChoiceFilterView + enabled: false + optional: true + choices: @statuses + + + @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/codingrules/app" + + jQuery.when(appXHR) + .done (r) -> + App.appState = new Backbone.Model + App.state = new Backbone.Model + App.qualityProfiles = r.qualityprofiles + App.languages = r.languages + App.repositories = r.repositories + App.statuses = r.statuses + App.tags = r.tags + App.characteristics = r.characteristics + window.messages = r.messages + + # Remove the initial spinner + jQuery('#coding-rules-page-loader').remove() + + # Start the application + App.start() \ No newline at end of file diff --git a/sonar-server/src/main/coffee/coding-rules/layout.coffee b/sonar-server/src/main/coffee/coding-rules/layout.coffee new file mode 100644 index 00000000000..dbe8633a152 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/layout.coffee @@ -0,0 +1,29 @@ +define [ + 'backbone.marionette', + 'templates/coding-rules' +], ( + Marionette, + Templates +) -> + + class AppLayout extends Marionette.Layout + className: 'navigator coding-rules-navigator' + template: Templates['coding-rules-layout'] + spinner: '' + + + regions: + headerRegion: '.navigator-header' + actionsRegion: '.navigator-actions' + resultsRegion: '.navigator-results' + detailsRegion: '.navigator-details' + filtersRegion: '.navigator-filters' + + + onRender: -> + # Adjust details region height + @$(@detailsRegion.el).css 'bottom', jQuery('#footer').outerHeight() + + + showSpinner: (region) -> + @$(@[region].el).html @spinner diff --git a/sonar-server/src/main/coffee/coding-rules/mockjax.coffee b/sonar-server/src/main/coffee/coding-rules/mockjax.coffee new file mode 100644 index 00000000000..2163fac2a64 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/mockjax.coffee @@ -0,0 +1,332 @@ +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' + '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.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.found': 'Found' + 'coding_rules._inherits': 'inherits' + 'coding_rules.key': 'Key:' + 'coding_rules.new_search': 'New Search' + 'coding_rules.no_results': 'No Coding Rules' + '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.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' + + + + # 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' + tags: ['bug', 'comment', 'java8'] + 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' } + ] + description: ''' +

+ According to the Java Language Specification: +

+ +
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.
+            
+ +

The following code snippet illustrates this rule:

+ +
public int getVector()[] { /* ... */ }    // Non-Compliant
+
+            public int[] getVector() { /* ... */ }    // Compliant
+
+            public int[] getMatrix()[] { /* ... */ }  // Non-Compliant
+
+            public int[][] getMatrix() { /* ... */ }  // Compliant
+            
''' + extra: '''This note is here only for test purposes.''' + extraRaw: '''This note is here *only for test purposes*.''' + + qualityProfiles: [ + { + name: 'SonarWay' + key: 'sonarway' + severity: 'MINOR' + parameters: [ + { key: 'someParameter', value: 8 } + ] + note: + username: 'Admin Admin' + html: '''

This note is here only for test purposes.

''' + raw: '''This note is here *only for test purposes*.''' + fCreationDate: 'less than a minute' + }, + { + name: 'Quality Profile 1' + key: 'qualityprofile1' + severity: 'MAJOR' + parameters: [ + { key: 'someParameter', value: 6 } + ] + inherits: 'sonarway' + } + ] + + + + # POST /api/codingrules/extend_description + jQuery.mockjax + url: "#{baseUrl}/api/codingrules/extend_description" + responseText: JSON.stringify + extra: '''This note is here only for test purposes.''' + 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: '''

This note is here only for test purposes.

''' + 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/sonar-server/src/main/coffee/coding-rules/router.coffee b/sonar-server/src/main/coffee/coding-rules/router.coffee new file mode 100644 index 00000000000..1788ce1799e --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/router.coffee @@ -0,0 +1,36 @@ +define [ + 'backbone', +], ( + Backbone, +) -> + + class AppRouter extends Backbone.Router + + routes: + '': 'index' + ':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: -> + @navigate '', trigger: true, replace: true + + + index: (query) -> + params = this.parseQuery(query) + @loadResults(params) + + + loadResults: (params) -> + @app.filterBarView.restoreFromQuery(params) + @app.restoreSorting(params) + @app.fetchFirstPage() diff --git a/sonar-server/src/main/coffee/coding-rules/views/actions-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/actions-view.coffee new file mode 100644 index 00000000000..96e306ed8e8 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/actions-view.coffee @@ -0,0 +1,62 @@ +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 + @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, + paging: @collection.paging + sorting: @collection.sorting diff --git a/sonar-server/src/main/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee new file mode 100644 index 00000000000..881b70c6293 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-bulk-change-dropdown-view.coffee @@ -0,0 +1,51 @@ +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' + unless param + @options.app.codingRulesBulkChangeView.show action + else + query = @options.app.getQuery() + switch action + when 'activate' then _.extend query, bulk_activate: [param] + when 'deactivate' then _.extend query, bulk_deactivate: [param] + @options.app.codingRulesBulkChangeView.bulkChange query + + + onRender: -> + jQuery('body').append @el + jQuery('body').off('click.bulk-change').on 'click.bulk-change', => @hide() + + + toggle: -> + if @$el.is(':visible') then @hide() else @show() + + + show: -> + @render() + @$el.show() + + + hide: -> + @$el.hide() + + + serializeData: -> + qualityProfile: @options.app.getQualityProfile() + qualityProfileName: @options.app.qualityProfileFilter.view.renderValue() \ No newline at end of file diff --git a/sonar-server/src/main/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee new file mode 100644 index 00000000000..144cbfafb37 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-bulk-change-view.coffee @@ -0,0 +1,103 @@ +define [ + 'backbone.marionette', + 'templates/coding-rules' +], ( + Marionette, + Templates +) -> + + class CodingRulesBulkChangeView extends Marionette.ItemView + className: 'modal' + template: Templates['coding-rules-bulk-change'] + + + events: + 'submit form': 'onSubmit' + 'click #coding-rules-cancel-bulk-change': 'hide' + '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-activate-on, #coding-rules-bulk-change-deactivate-on').select2 + width: '250px' + minimumResultsForSearch: 1 + + format = (state) -> + return state.text unless state.id + " #{state.text}" + @$('#coding-rules-bulk-change-severity').select2 + width: '250px' + minimumResultsForSearch: 999 + formatResult: format + formatSelection: format + escapeMarkup: (m) -> m + + + show: (action) -> + @action = action + @render() + @$el.dialog 'open' + + + hide: -> + @$el.dialog 'close' + + + prepareQuery: -> + query = @options.app.getQuery() + + if @action == 'activate' + if @$('#coding-rules-bulk-change-activate-all').is ':checked' + _.extend query, bulk_activate: _.pluck @options.app.qualityProfiles, 'key' + else + _.extend query, bulk_activate: @$('#coding-rules-bulk-change-activate-on').val() + + if @action == 'deactivate' + if @$('#coding-rules-bulk-change-deactivate-all').is ':checked' + _.extend query, bulk_deactivate: _.pluck @options.app.qualityProfiles, 'key' + else + _.extend query, bulk_deactivate: @$('#coding-rules-bulk-change-deactivate-on').val() + + if @action == 'change-severity' + _.extend query, bulk_change_severity: @$('#coding-rules-bulk-change-severity').val() + + query + + + bulkChange: (query) -> + jQuery.ajax + type: 'POST' + url: "#{baseUrl}/api/codingrules/bulk_change" + data: query + .done => + @options.app.fetchFirstPage() + + + onSubmit: (e) -> + e.preventDefault() + @bulkChange(@prepareQuery()).done => @hide() + + + serializeData: -> + action: @action + + paging: @options.app.codingRules.paging + qualityProfiles: @options.app.qualityProfiles + + qualityProfile: @options.app.getQualityProfile() + qualityProfileName: @options.app.qualityProfileFilter.view.renderValue() + + activateOnQualityProfiles: @options.app.qualityProfiles + deactivateOnQualityProfiles: _.reject @options.app.qualityProfiles, (q) => q.key == @options.app.getQualityProfile() + + severities: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'] diff --git a/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee new file mode 100644 index 00000000000..063b8378929 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profile-view.coffee @@ -0,0 +1,47 @@ +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'] + + + ui: + change: '.coding-rules-detail-quality-profile-change' + + + events: + 'click @ui.change': 'change' + + + change: -> + @options.app.codingRulesQualityProfileActivationView.model = @model + @options.app.codingRulesQualityProfileActivationView.show() + + + enableUpdate: -> + @ui.update.prop 'disabled', false + + + getParent: -> + return null unless @model.get 'inherits' + @options.qualityProfiles.findWhere(key: @model.get('inherits')).toJSON() + + + enhanceParameters: -> + parent = @getParent() + parameters = @model.get 'parameters' + return parameters unless parent + parameters.map (p) -> + _.extend p, original: _.findWhere(parent.parameters, key: p.key).value + + + serializeData: -> + _.extend super, + parent: @getParent() + parameters: @enhanceParameters() \ No newline at end of file diff --git a/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee new file mode 100644 index 00000000000..71b0b311062 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-quality-profiles-view.coffee @@ -0,0 +1,14 @@ +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 + qualityProfiles: @collection \ No newline at end of file diff --git a/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-view.coffee new file mode 100644 index 00000000000..4cf361c7a5c --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-detail-view.coffee @@ -0,0 +1,131 @@ +define [ + 'backbone', + 'backbone.marionette', + 'coding-rules/views/coding-rules-detail-quality-profiles-view' + 'templates/coding-rules' +], ( + Backbone, + Marionette, + CodingRulesDetailQualityProfilesView, + Templates +) -> + + class CodingRulesDetailView extends Marionette.Layout + template: Templates['coding-rules-detail'] + + + regions: + qualityProfilesRegion: '#coding-rules-detail-quality-profiles' + + + 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' + 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' + changeQualityProfile: '.coding-rules-detail-quality-profile-update' + + + events: + 'click @ui.tagsChange': 'changeTags' + 'click @ui.tagsEditDone': 'editDone' + + 'click @ui.extendDescriptionLink': 'showExtendDescriptionForm' + 'click @ui.cancelExtendDescription': 'hideExtendDescriptionForm' + 'click @ui.extendDescriptionSubmit': 'submitExtendDescription' + + 'click @ui.activateQualityProfile': 'activateQualityProfile' + 'click @ui.changeQualityProfile': 'changeQualityProfile' + + + initialize: (options) -> + @qualityProfilesView = new CodingRulesDetailQualityProfilesView + app: @options.app + collection: new Backbone.Collection options.model.get 'qualityProfiles' + + + onRender: -> + @qualityProfilesRegion.show @qualityProfilesView + + @ui.tagInput.select2 + tags: _.difference @options.app.tags, @model.get 'tags' + width: '300px' + @ui.tagsEdit.hide() + + @ui.extendDescriptionForm.hide() + @ui.extendDescriptionSpinner.hide() + + + changeTags: -> + @ui.tagsEdit.show() + @ui.tagsList.hide() + + + editDone: -> + @ui.tagsEdit.html '' + tags = @ui.tagInput.val() + jQuery.ajax + type: 'POST' + url: "#{baseUrl}/api/codingrules/set_tags" + data: tags: tags + .done => + @model.set 'tags', tags.split ',' + @render() + + + showExtendDescriptionForm: -> + @ui.descriptionExtra.hide() + @ui.extendDescriptionForm.show() + + + hideExtendDescriptionForm: -> + @ui.descriptionExtra.show() + @ui.extendDescriptionForm.hide() + + + submitExtendDescription: -> + @ui.extendDescriptionForm.hide() + @ui.extendDescriptionSpinner.show() + jQuery.ajax + type: 'POST' + url: "#{baseUrl}/api/codingrules/extend_description" + dataType: 'json' + data: text: @ui.extendDescriptionText.val() + .done (r) => + @model.set extra: r.extra, extraRaw: r.extraRaw + @render() + + + getContextQualilyProfile: -> + contextQualityProfile = @options.app.getQualityProfile() + _.findWhere @model.get('qualityProfiles'), key: contextQualityProfile + + + activateQualityProfile: -> + @options.app.codingRulesQualityProfileActivationView.model = null + @options.app.codingRulesQualityProfileActivationView.show() + + + changeQualityProfile: -> + @options.app.codingRulesQualityProfileActivationView.model = new Backbone.Model @getContextQualilyProfile() + @options.app.codingRulesQualityProfileActivationView.show() + + + serializeData: -> + contextQualityProfile = @options.app.getQualityProfile() + + _.extend super, + contextQualityProfile: contextQualityProfile + contextQualityProfileName: @options.app.qualityProfileFilter.view.renderValue() + qualityProfile: @getContextQualilyProfile() \ No newline at end of file diff --git a/sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-empty-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-empty-view.coffee new file mode 100644 index 00000000000..c3eb8d48c4e --- /dev/null +++ b/sonar-server/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/sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-item-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-item-view.coffee new file mode 100644 index 00000000000..69a9e5fdaff --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-item-view.coffee @@ -0,0 +1,34 @@ +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: -> + @$el.siblings().removeClass @activeClass + @$el.addClass @activeClass + + @options.app.layout.showSpinner 'detailsRegion' + jQuery.ajax + url: "#{baseUrl}/api/codingrules/show" + .done (r) => + @model.set r.codingrule + @options.app.codingRulesQualityProfileActivationView.rule = @model + detailView = new CodingRulesDetailView + app: @options.app + model: @model + @options.app.layout.detailsRegion.show detailView diff --git a/sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-view.coffee new file mode 100644 index 00000000000..93db305b2b5 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-list-view.coffee @@ -0,0 +1,23 @@ +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 + + + selectFirst: -> + @$el.find('*:first').click() diff --git a/sonar-server/src/main/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee new file mode 100644 index 00000000000..70aa55a5daf --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/coding-rules-quality-profile-activation-view.coffee @@ -0,0 +1,85 @@ +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' + + + events: + 'click #coding-rules-quality-profile-activation-cancel': 'hide' + 'click @ui.qualityProfileActivate': 'activate' + + + activate: -> + @$('.modal-foot').html '' + jQuery.ajax + type: 'POST' + url: "#{baseUrl}/api/codingrules/activate" + data: id: 1 + .done => + jQuery('.navigator-results-list .active').click() + @hide() + + + 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 + " #{state.text}" + + severity = if @model then @model.get 'severity' else @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: -> + _.reject @options.app.qualityProfiles, (profile) => + _.findWhere @rule.get('qualityProfiles'), key: profile.key + + + serializeData: -> + parameters = if @model then @model.get('parameters') else @rule.get('parameters') + + _.extend super, + rule: @rule.toJSON() + parameters: parameters + qualityProfiles: @getAvailableQualityProfiles() + severities: ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'] diff --git a/sonar-server/src/main/coffee/coding-rules/views/filter-bar-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/filter-bar-view.coffee new file mode 100644 index 00000000000..6a719be85aa --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/filter-bar-view.coffee @@ -0,0 +1,71 @@ +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' + + + 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: -> + @options.app.state.set + query: this.options.app.getQuery(), + search: true + @options.app.fetchFirstPage() + + + fetchNextPage: -> + @options.app.fetchNextPage() diff --git a/sonar-server/src/main/coffee/coding-rules/views/filters/activation-filter-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/filters/activation-filter-view.coffee new file mode 100644 index 00000000000..896ee64c050 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/filters/activation-filter-view.coffee @@ -0,0 +1,41 @@ +define [ + 'navigator/filters/choice-filters' + 'coding-rules/views/filters/inheritance-filter-view' +], ( + ChoiceFilters + InheritanceFilterView +) -> + + class DetailsActivationFilterView extends ChoiceFilters.DetailsChoiceFilterView + + onCheck: (e) -> + id = jQuery(e.target).val() + selected = @options.filterView.choices.findWhere checked: true + unless id == selected + @options.filterView.choices.each (item) -> item.set 'checked', item.id == id + else + e.preventDefault() + @updateValue() + @updateLists() + + + + class ActivationFilterView extends InheritanceFilterView + tooltip: 'coding_rules.filters.activation.help' + + + initialize: -> + super detailsView: DetailsActivationFilterView + + + onChangeQualityProfile: -> + qualityProfile = @qualityProfileFilter.get 'value' + if _.isArray(qualityProfile) && qualityProfile.length == 1 then @makeActive() else @makeInactive() + + + makeActive: -> + @choices.each (item) -> item.set 'checked', item.id == 'active' + @detailsView.updateValue() + @detailsView.updateLists() + @render() + super \ No newline at end of file diff --git a/sonar-server/src/main/coffee/coding-rules/views/filters/characteristic-filter-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/filters/characteristic-filter-view.coffee new file mode 100644 index 00000000000..efa29a733ff --- /dev/null +++ b/sonar-server/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/sonar-server/src/main/coffee/coding-rules/views/filters/inheritance-filter-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/filters/inheritance-filter-view.coffee new file mode 100644 index 00000000000..8e575c20268 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/filters/inheritance-filter-view.coffee @@ -0,0 +1,52 @@ +define [ + 'navigator/filters/choice-filters' +], ( + ChoiceFilters +) -> + + class InheritanceFilterView extends ChoiceFilters.ChoiceFilterView + tooltip: 'coding_rules.filters.inheritance.inactive' + + + initialize: -> + super + @qualityProfileFilter = @model.get 'qualityProfileFilter' + @listenTo @qualityProfileFilter, 'change:value', @onChangeQualityProfile + @onChangeQualityProfile() + + + onChangeQualityProfile: -> + qualityProfile = @qualityProfileFilter.get 'value' + parentQualityProfile = @qualityProfileFilter.get 'parentQualityProfile' + if _.isArray(qualityProfile) && qualityProfile.length == 1 && parentQualityProfile + @makeActive() + else + @makeInactive() + + + makeActive: -> + @model.set inactive: false, title: '' + @model.trigger 'change:enabled' + @$el.removeClass('navigator-filter-inactive').prop 'title', '' + + + 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 + @onChangeQualityProfile() + else + @clear() diff --git a/sonar-server/src/main/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee new file mode 100644 index 00000000000..eff55a9e618 --- /dev/null +++ b/sonar-server/src/main/coffee/coding-rules/views/filters/quality-profile-filter-view.coffee @@ -0,0 +1,51 @@ +define [ + 'navigator/filters/ajax-select-filters' +], ( + AjaxSelectFilters +) -> + + class QualityProfileSuggestions extends AjaxSelectFilters.Suggestions + + url: -> + "#{baseUrl}/api/qualityprofiles/list" + + + + class QualityProfileFilterView extends AjaxSelectFilters.AjaxSelectFilterView + + initialize: -> + super + @choices = new QualityProfileSuggestions + @listenTo @model, 'change:value', @onValueChange + + + onValueChange: -> + @updateParentQualityProfile() + @highlightContext() + + + updateParentQualityProfile: -> + selected = @getSelected() + if selected.length == 1 + @model.set 'parentQualityProfile', selected[0].get('parent') + else + @model.unset 'parentQualityProfile' + + + highlightContext: -> + hasContext = _.isArray(@model.get('value')) && @model.get('value').length > 0 + @$el.toggleClass 'navigator-filter-context', hasContext + + + createRequest: (v) -> + jQuery.ajax + url: baseUrl + '/api/qualityprofiles/show' + type: 'GET' + data: key: v + .done (r) => + @choices.add new Backbone.Model + id: r.qualityprofile.id, + text: r.qualityprofile.text, + parent: r.qualityprofile.parent, + checked: true + diff --git a/sonar-server/src/main/coffee/coding-rules/views/header-view.coffee b/sonar-server/src/main/coffee/coding-rules/views/header-view.coffee new file mode 100644 index 00000000000..ae0f8718732 --- /dev/null +++ b/sonar-server/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.navigate '', trigger: true diff --git a/sonar-server/src/main/coffee/common/inputs.coffee b/sonar-server/src/main/coffee/common/inputs.coffee new file mode 100644 index 00000000000..4352040b021 --- /dev/null +++ b/sonar-server/src/main/coffee/common/inputs.coffee @@ -0,0 +1,84 @@ +$ = jQuery + +transformPattern = (pattern) -> + return pattern.replace /\{0\}/g, '(\\d+)' + + +convertWorkDuration = (value) -> + daysPattern = transformPattern t('work_duration.x_days') + hoursPattern = transformPattern t('work_duration.x_hours') + minutesPattern = transformPattern t('work_duration.x_minutes') + + days = value.match daysPattern + hours = value.match hoursPattern + minutes = value.match minutesPattern + + days = if days then +days[1] else 0 + hours = if hours then +hours[1] else 0 + minutes = if minutes then +minutes[1] else 0 + + if !value || (value.length > 0 && days == 0 && hours == 0 && minutes == 0) + value + else + (days * 8 + hours) * 60 + minutes + + +restoreWorkDuration = (value) -> + return value unless /^\d+$/.test value + days = Math.floor(value / (8 * 60)) + hours = Math.floor((value - days * 8 * 60) / 60) + minutes = value % 60 + result = [] + result.push t('work_duration.x_days').replace('{0}', days) if days > 0 + result.push t('work_duration.x_hours').replace('{0}', hours) if hours > 0 + result.push t('work_duration.x_minutes').replace('{0}', minutes) if minutes > 0 + result.join ' ' + + +convertRating = (value) -> + if /^[ABCDE]$/.test(value) + value.charCodeAt(0) - 'A'.charCodeAt(0) + 1 + else + value + + +convertValue = (value, input) -> + type = input.data 'type' + + # No convertation if input doesn't have data-type + return value unless type? + + # Do necessary convertion depeneds on input data-type + switch type + when 'WORK_DUR' then convertWorkDuration value + when 'RATING' then convertRating value + else value + + +restoreRating = (value) -> + return value unless /^[12345]+$/.test value + String.fromCharCode(value - 1 + 'A'.charCodeAt(0)) + + +restoreValue = (value, input) -> + type = input.data 'type' + + # No convertation if input doesn't have data-type + return value unless type? + + # Do necessary convertion depeneds on input data-type + switch type + when 'WORK_DUR' then restoreWorkDuration value + when 'RATING' then restoreRating value + else value + + +originalVal = $.fn.val +$.fn.val = (value) -> + if arguments.length + originalVal.call @, (restoreValue value, @) + else + convertValue originalVal.call(@), @ + +$.fn.originalVal = originalVal + diff --git a/sonar-server/src/main/coffee/quality-gate/app.coffee b/sonar-server/src/main/coffee/quality-gate/app.coffee new file mode 100644 index 00000000000..db18db98711 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/app.coffee @@ -0,0 +1,142 @@ +requirejs.config + baseUrl: "#{baseUrl}/js" + + paths: + 'jquery': 'third-party/jquery' + 'backbone': 'third-party/backbone' + 'backbone.marionette': 'third-party/backbone.marionette' + 'handlebars': 'third-party/handlebars' + 'moment': 'third-party/moment' + 'select-list': 'common/select-list' + + shim: + 'backbone.marionette': + deps: ['backbone'] + exports: 'Marionette' + 'backbone': + exports: 'Backbone' + 'handlebars': + exports: 'Handlebars' + 'moment': + exports: 'moment' + 'select-list': + exports: 'SelectList' + + +requirejs [ + 'backbone', 'backbone.marionette', 'handlebars', + 'quality-gate/collections/quality-gates', + 'quality-gate/views/quality-gate-sidebar-list-view', + 'quality-gate/views/quality-gate-actions-view', + 'quality-gate/views/quality-gate-edit-view', + 'quality-gate/router', + 'quality-gate/layout', + 'common/handlebars-extensions' +], ( + Backbone, Marionette, Handlebars, + QualityGates, + QualityGateSidebarListItemView, + QualityGateActionsView, + QualityGateEditView, + QualityGateRouter, + QualityGateLayout +) -> + + # 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 '. ' + 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 quality-gates-page'); + + + # Create a Quality Gate Application + App = new Marionette.Application + + + App.qualityGates = new QualityGates + + + App.openFirstQualityGate = -> + if @qualityGates.length > 0 + @router.navigate "show/#{@qualityGates.models[0].get('id')}", trigger: true + else + App.layout.detailsRegion.reset() + + + App.deleteQualityGate = (id) -> + App.qualityGates.remove id + App.openFirstQualityGate() + + + App.unsetDefaults = (id) -> + App.qualityGates.each (gate) -> + gate.set('default', false) unless gate.id == id + + + # Construct layout + App.addInitializer -> + @layout = new QualityGateLayout app: @ + jQuery('body').append @layout.render().el + + + # Construct actions bar + App.addInitializer -> + @codingRulesHeaderView = new QualityGateActionsView + app: @ + @layout.actionsRegion.show @codingRulesHeaderView + + + # Construct sidebar + App.addInitializer -> + @qualityGateSidebarListView = new QualityGateSidebarListItemView + collection: @qualityGates + app: @ + @layout.resultsRegion.show @qualityGateSidebarListView + + + # Construct edit view + App.addInitializer -> + @qualityGateEditView = new QualityGateEditView app: @ + @qualityGateEditView.render() + + + # Start router + App.addInitializer -> + @router = new QualityGateRouter app: @ + Backbone.history.start() + + + # Open first quality gate when come to the page + App.addInitializer -> + initial = Backbone.history.fragment == '' + App.openFirstQualityGate() if initial + + + # Call app, Load metrics and the list of quality gates before start the application + appXHR = jQuery.ajax + url: "#{baseUrl}/api/qualitygates/app" + .done (r) => + App.canEdit = r.edit + App.periods = r.periods + App.metrics = r.metrics + window.messages = r.messages + + qualityGatesXHR = App.qualityGates.fetch() + + jQuery.when(qualityGatesXHR, appXHR) + .done -> + # Remove the initial spinner + jQuery('#quality-gate-page-loader').remove() + + # Start the application + App.start() diff --git a/sonar-server/src/main/coffee/quality-gate/collections/conditions.coffee b/sonar-server/src/main/coffee/quality-gate/collections/conditions.coffee new file mode 100644 index 00000000000..8899fc14405 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/collections/conditions.coffee @@ -0,0 +1,11 @@ +define [ + 'backbone', + 'quality-gate/models/condition' +], ( + Backbone, + Condition +) -> + + class Conditions extends Backbone.Collection + model: Condition + comparator: 'metric' diff --git a/sonar-server/src/main/coffee/quality-gate/collections/quality-gates.coffee b/sonar-server/src/main/coffee/quality-gate/collections/quality-gates.coffee new file mode 100644 index 00000000000..1d1c8542481 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/collections/quality-gates.coffee @@ -0,0 +1,30 @@ +define [ + 'backbone', + 'quality-gate/models/quality-gate' +], ( + Backbone, + QualityGate +) -> + + class QualityGates extends Backbone.Collection + model: QualityGate + + + url: -> + "#{baseUrl}/api/qualitygates/list" + + + # { + # "qualitygates": [ + # { "id": 42, "name": "QG 1" }, + # { "id": 43, "name": "QG 2" }, + # { "id": 44, "name": "QG 3" } + # ], + # "default": 42 + # } + parse: (r) -> + r.qualitygates.map (gate) -> + _.extend gate, default: gate.id == r.default + + + comparator: (item) -> item.get('name').toLowerCase() diff --git a/sonar-server/src/main/coffee/quality-gate/layout.coffee b/sonar-server/src/main/coffee/quality-gate/layout.coffee new file mode 100644 index 00000000000..ff7f6f54bbd --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/layout.coffee @@ -0,0 +1,35 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' +], ( + Marionette, + Templates +) -> + + class AppLayout extends Marionette.Layout + className: 'navigator quality-gates-navigator' + template: Templates['quality-gates-layout'] + + + regions: + headerRegion: '.navigator-header' + actionsRegion: '.navigator-actions' + resultsRegion: '.navigator-results' + detailsRegion: '.navigator-details' + + + initialize: (options) -> + @listenTo options.app.qualityGates, 'all', @updateLayout + + + updateLayout: -> + empty = @options.app.qualityGates.length == 0 + @$(@headerRegion.el).toggle !empty + @$(@detailsRegion.el).toggle !empty + + + onRender: -> + @updateLayout() + + # Adjust details region height + @$(@detailsRegion.el).css 'bottom', jQuery('#footer').outerHeight() diff --git a/sonar-server/src/main/coffee/quality-gate/models/condition.coffee b/sonar-server/src/main/coffee/quality-gate/models/condition.coffee new file mode 100644 index 00000000000..1fe2c634a41 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/models/condition.coffee @@ -0,0 +1,42 @@ +define [ + 'backbone' +], ( + Backbone +) -> + + class Condition extends Backbone.Model + + url: -> + "#{baseUrl}/api/qualitygates/create_condition" + + + save: -> + method = unless @isNew() then 'update' else 'create' + data = + metric: @get('metric').key + op: @get('op') + warning: @get('warning') + error: @get('error') + + unless @get('period') == '0' + data.period = @get('period') + + unless @isNew() + data.id = @id + else + data.gateId = @get('gateId') + + jQuery.ajax({ + url: "#{baseUrl}/api/qualitygates/#{method}_condition" + type: 'POST' + data: data + }).done (r) => + @set 'id', r.id + + + delete: -> + jQuery.ajax + url: "#{baseUrl}/api/qualitygates/delete_condition" + type: 'POST' + data: id: @id + diff --git a/sonar-server/src/main/coffee/quality-gate/models/quality-gate.coffee b/sonar-server/src/main/coffee/quality-gate/models/quality-gate.coffee new file mode 100644 index 00000000000..5acffc066e7 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/models/quality-gate.coffee @@ -0,0 +1,10 @@ +define [ + 'backbone' +], ( + Backbone +) -> + + class QualityGate extends Backbone.Model + + url: -> + "#{baseUrl}/api/qualitygates/show?id=#{@get('id')}" diff --git a/sonar-server/src/main/coffee/quality-gate/router.coffee b/sonar-server/src/main/coffee/quality-gate/router.coffee new file mode 100644 index 00000000000..f21161a97bf --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/router.coffee @@ -0,0 +1,42 @@ +define [ + 'backbone', + 'quality-gate/models/quality-gate', + 'quality-gate/views/quality-gate-detail-view', + 'quality-gate/views/quality-gate-detail-header-view', +], ( + Backbone, + QualityGate, + QualityGateDetailView, + QualityGateDetailHeaderView +) -> + + class QualityGateRouter extends Backbone.Router + + routes: + 'show/:id': 'show' + + + initialize: (options) -> + @app = options.app + + + show: (id) -> + qualityGate = @app.qualityGates.get id + if qualityGate + @app.qualityGateSidebarListView.highlight id + + qualityGateDetailHeaderView = new QualityGateDetailHeaderView + app: @app + model: qualityGate + @app.layout.headerRegion.show qualityGateDetailHeaderView + + qualityGateDetailView = new QualityGateDetailView + app: @app + model: qualityGate + @app.layout.detailsRegion.show qualityGateDetailView + qualityGateDetailView.$el.hide() + + qualityGateDetailHeaderView.showSpinner() + qualityGate.fetch().done -> + qualityGateDetailView.$el.show() + qualityGateDetailHeaderView.hideSpinner() diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-actions-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-actions-view.coffee new file mode 100644 index 00000000000..6025a3c5b3c --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-actions-view.coffee @@ -0,0 +1,27 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' + 'quality-gate/models/quality-gate' +], ( + Marionette, + Templates + QualityGate +) -> + + class QualityGateActionsView extends Marionette.ItemView + template: Templates['quality-gate-actions'] + + + events: + 'click #quality-gate-add': 'add' + + + add: -> + qualityGate = new QualityGate() + @options.app.qualityGateEditView.method = 'create' + @options.app.qualityGateEditView.model = qualityGate + @options.app.qualityGateEditView.show() + + + serializeData: -> + _.extend super, canEdit: @options.app.canEdit diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee new file mode 100644 index 00000000000..e928b169b25 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-condition-view.coffee @@ -0,0 +1,123 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' +], ( + Marionette, + Templates +) -> + + class QualityGateDetailConditionView extends Marionette.ItemView + tagName: 'tr' + template: Templates['quality-gate-detail-condition'] + spinner: '' + + + modelEvents: + 'change:id': 'render' + + + ui: + periodSelect: '[name=period]' + operatorSelect: '[name=operator]' + warningInput: '[name=warning]' + errorInput: '[name=error]' + actionsBox: '.quality-gate-condition-actions' + updateButton: '.update-condition' + + + events: + 'click @ui.updateButton': 'saveCondition' + 'click .delete-condition': 'deleteCondition' + 'click .add-condition': 'saveCondition' + 'click .cancel-add-condition': 'cancelAddCondition' + 'keyup :input': 'enableUpdate' + 'change :input': 'enableUpdate' + + + initialize: -> + @populateMetric() + + + populateMetric: -> + metricKey = @model.get('metric') + metric = _.findWhere @options.app.metrics, key: metricKey + if metric? + switch metric.type + when 'WORK_DUR' then metric.placeholder = '1d 7h 59min' + when 'RATING' then metric.placeholder = 'A' + @model.set { metric: metric }, { silent: true } + @model.set { isDiffMetric: metric.key.indexOf('new_') == 0 }, { silent: true } + + + onRender: -> + @ui.periodSelect.val @model.get('period') || '0' + @ui.operatorSelect.val @model.get('op') + @ui.warningInput.val @model.get('warning') + @ui.errorInput.val @model.get('error') + + @ui.periodSelect.select2 + allowClear: false + minimumResultsForSearch: 999 + width: '200px' + + @ui.operatorSelect.select2 + allowClear: false + minimumResultsForSearch: 999 + width: '150px' + + @ui.periodSelect.select2('open') if @model.isNew() + + + showSpinner: -> + jQuery(@spinner).prependTo @ui.actionsBox + @ui.actionsBox.find(':not(.spinner)').hide() + + + hideSpinner: -> + @ui.actionsBox.find('.spinner').remove() + @ui.actionsBox.find(':not(.spinner)').show() + + + saveCondition: -> + @showSpinner() + @model.set + period: @ui.periodSelect.val() + op: @ui.operatorSelect.val() + warning: @ui.warningInput.val() + error: @ui.errorInput.val() + @model.save() + .always => + @ui.updateButton.prop 'disabled', true + @hideSpinner() + .done => + @options.collectionView.updateConditions() + + + deleteCondition: -> + if confirm t('quality_gates.delete_condition.confirm.message') + @showSpinner() + @model.delete().done => + @options.collectionView.collection.remove @model + @options.collectionView.updateConditions() + @close() + + + cancelAddCondition: -> + @close() + + + enableUpdate: -> + @ui.updateButton.prop 'disabled', false + + + serializeData: -> + period = _.findWhere(@options.app.periods, key: this.model.get('period')) + data = _.extend super, + canEdit: @options.app.canEdit + periods: @options.app.periods + periodText: period?.text + unless @options.app.canEdit + _.extend data, + warning: jQuery('').data('type', @model.get('metric').type).val(@model.get('warning')).originalVal() + error: jQuery('').data('type', @model.get('metric').type).val(@model.get('error')).originalVal() + data diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee new file mode 100644 index 00000000000..9c0362f4e6a --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-empty-view.coffee @@ -0,0 +1,11 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' +], ( + Marionette, + Templates +) -> + + class QualityGateDetailConditionsView extends Marionette.ItemView + tagName: 'tr' + template: Templates['quality-gate-detail-conditions-empty'] diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee new file mode 100644 index 00000000000..7b76a55ae61 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-conditions-view.coffee @@ -0,0 +1,86 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' + 'quality-gate/models/condition', + 'quality-gate/views/quality-gate-detail-condition-view', + 'quality-gate/views/quality-gate-detail-conditions-empty-view' +], ( + Marionette, + Templates + Condition, + QualityGateDetailConditionView, + QualityGateDetailConditionsEmptyView, +) -> + + class QualityGateDetailConditionsView extends Marionette.CompositeView + template: Templates['quality-gate-detail-conditions'] + itemView: QualityGateDetailConditionView + emptyView: QualityGateDetailConditionsEmptyView + itemViewContainer: '.quality-gate-conditions tbody' + + + ui: + metricSelect: '#quality-gate-new-condition-metric' + introductionShowMore: '.quality-gate-introduction-show-more' + introductionMore: '.quality-gate-introduction-more' + + + events: + 'click @ui.introductionShowMore': 'showMoreIntroduction' + 'change @ui.metricSelect': 'addCondition' + + + itemViewOptions: -> + app: @options.app + collectionView: @ + + + appendHtml: (compositeView, itemView) -> + if (compositeView.isBuffering) + compositeView.elBuffer.appendChild itemView.el + compositeView._bufferedChildren.push itemView + else + container = @getItemViewContainer compositeView + container.prepend itemView.el + + + onRender: -> + @ui.introductionMore.hide() + @ui.metricSelect.select2 + allowClear: false, + width: '250px', + placeholder: t('alerts.select_metric') + + + groupedMetrics: -> + metrics = @options.app.metrics + metrics = _.groupBy metrics, 'domain' + metrics = _.map metrics, (metrics, domain) -> + domain: domain, metrics: _.sortBy metrics, 'short_name' + _.sortBy metrics, 'domain' + + + serializeData: -> + _.extend super, + canEdit: @options.app.canEdit + metricGroups: @groupedMetrics() + + + showMoreIntroduction: -> + @ui.introductionShowMore.hide() + @ui.introductionMore.show() + + + addCondition: -> + metric = @ui.metricSelect.val() + @ui.metricSelect.select2('val', '') + condition = new Condition + metric: metric + gateId: @options.gateId + @collection.unshift condition + + + updateConditions: -> + conditions = @collection.map (item) -> _.extend item.toJSON(), + metric: item.get('metric').key + @options.qualityGate.set { conditions: conditions }, { silent: true } diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-header-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-header-view.coffee new file mode 100644 index 00000000000..acf593f149f --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-header-view.coffee @@ -0,0 +1,90 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates', + 'quality-gate/models/quality-gate' +], ( + Marionette, + Templates + QualityGate +) -> + + class QualityGateDetailHeaderView extends Marionette.ItemView + template: Templates['quality-gate-detail-header'] + spinner: '' + + + modelEvents: + 'change': 'render' + + + events: + 'click #quality-gate-rename': 'renameQualityGate' + 'click #quality-gate-copy': 'copyQualityGate' + 'click #quality-gate-delete': 'deleteQualityGate' + 'click #quality-gate-set-as-default': 'setAsDefault' + 'click #quality-gate-unset-as-default': 'unsetAsDefault' + + + renameQualityGate: -> + @options.app.qualityGateEditView.method = 'rename' + @options.app.qualityGateEditView.model = @model + @options.app.qualityGateEditView.show() + + + copyQualityGate: -> + copiedModel = new QualityGate @model.toJSON() + copiedModel.set 'default', false + @options.app.qualityGateEditView.method = 'copy' + @options.app.qualityGateEditView.model = copiedModel + @options.app.qualityGateEditView.show() + + + deleteQualityGate: -> + message = if @model.get 'default' then 'quality_gates.delete.confirm.default' else 'quality_gates.delete.confirm.message' + if confirm t(message).replace('{0}', @model.get 'name') + @showSpinner() + jQuery.ajax + type: 'POST' + url: "#{baseUrl}/api/qualitygates/destroy" + data: id: @model.id + .always => + @hideSpinner() + .done => + @options.app.deleteQualityGate @model.id + + + changeDefault: (set) -> + @showSpinner() + data = if set then { id: @model.id } else {} + method = if set then 'set_as_default' else 'unset_default' + jQuery.ajax + type: 'POST' + url: "#{baseUrl}/api/qualitygates/#{method}" + data: data + .always => + @hideSpinner() + .done => + @options.app.unsetDefaults @model.id + @model.set 'default', !@model.get('default') + + + setAsDefault: -> + @changeDefault true + + + unsetAsDefault: -> + @changeDefault false + + + showSpinner: -> + @$el.hide() + jQuery(@spinner).insertBefore @$el + + + hideSpinner: -> + @$el.prev().remove() + @$el.show() + + + serializeData: -> + _.extend super, canEdit: @options.app.canEdit diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee new file mode 100644 index 00000000000..afa85c6bad7 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-projects-view.coffee @@ -0,0 +1,38 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' + 'select-list' +], ( + Marionette, + Templates +) -> + + class QualityGateDetailProjectsView extends Marionette.ItemView + template: Templates['quality-gate-detail-projects'] + + + onRender: -> + unless @model.get('default') + new SelectList + el: @$('#select-list-projects') + width: '100%' + readOnly: !@options.app.canEdit + format: (item) -> item.name + searchUrl: "#{baseUrl}/api/qualitygates/search?gateId=#{@options.gateId}" + selectUrl: "#{baseUrl}/api/qualitygates/select" + deselectUrl: "#{baseUrl}/api/qualitygates/deselect" + extra: + gateId: @options.gateId + selectParameter: 'projectId' + selectParameterValue: 'id' + labels: + selected: t('quality_gates.projects.with') + deselected: t('quality_gates.projects.without') + all: t('quality_gates.projects.all') + noResults: t('quality_gates.projects.noResults') + tooltips: + select: t('quality_gates.projects.select_hint') + deselect: t('quality_gates.projects.deselect_hint') + + serializeData: -> + _.extend super, canEdit: @options.app.canEdit diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-view.coffee new file mode 100644 index 00000000000..ff2ca9c70ea --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-detail-view.coffee @@ -0,0 +1,50 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates', + 'quality-gate/collections/conditions', + 'quality-gate/views/quality-gate-detail-header-view', + 'quality-gate/views/quality-gate-detail-conditions-view', + 'quality-gate/views/quality-gate-detail-projects-view' +], ( + Marionette, + Templates, + Conditions, + QualityGateDetailHeaderView, + QualityGateDetailConditionsView, + QualityGateDetailProjectsView +) -> + + class QualityGateDetailView extends Marionette.Layout + template: Templates['quality-gate-detail'] + + + regions: + conditionsRegion: '#quality-gate-conditions' + projectsRegion: '#quality-gate-projects' + + + modelEvents: + 'change': 'render' + + + onRender: -> + @showConditions() + @showProjects() + + + showConditions: -> + conditions = new Conditions @model.get('conditions') + view = new QualityGateDetailConditionsView + app: @options.app + collection: conditions + gateId: @model.id + qualityGate: @model + @conditionsRegion.show view + + + showProjects: -> + view = new QualityGateDetailProjectsView + app: @options.app + model: @model + gateId: @model.id + @projectsRegion.show view diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-edit-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-edit-view.coffee new file mode 100644 index 00000000000..47e0bbd9101 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-edit-view.coffee @@ -0,0 +1,86 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' +], ( + Marionette, + Templates +) -> + + class QualityGateEditView extends Marionette.ItemView + className: 'modal' + template: Templates['quality-gate-edit'] + + + ui: + nameInput: '#quality-gate-edit-name' + + + events: + 'submit form': 'onSubmit' + 'click #quality-gate-cancel-create': 'hide' + + + onRender: -> + @$el.dialog + dialogClass: 'no-close', + width: '600px', + draggable: false, + autoOpen: false, + modal: true, + minHeight: 50, + resizable: false, + title: null + + + show: -> + @render() + @$el.dialog 'open' + @ui.nameInput.focus() + + + hide: -> + @$el.dialog 'close' + + + saveRequest: (data) -> + jQuery.ajax + type: 'POST' + url: "#{baseUrl}/api/qualitygates/#{@method}" + data: data + .done => @hide() + + + onSubmit: (e) -> + e.preventDefault() + switch @method + when 'create' then @createQualityGate() + when 'copy' then @copyQualityGate() + when 'rename' then @saveQualityGate() + else + + + createQualityGate: -> + data = name: @ui.nameInput.val() + @saveRequest(data).done (r) => + @model.set id: r.id, name: r.name + @options.app.qualityGates.add @model + @options.app.router.navigate "show/#{r.id}", trigger: true + + + saveQualityGate: -> + data = id: @model.id, name: @ui.nameInput.val() + @saveRequest(data).done (r) => + @model.set name: r.name + + + copyQualityGate: -> + data = id: @model.id, name: @ui.nameInput.val() + @saveRequest(data).done (r) => + @model.set id: r.id, name: r.name + @options.app.qualityGates.add @model + @options.app.router.navigate "show/#{r.id}", trigger: true + + + serializeData: -> + if @model + _.extend @model.toJSON(), method: @method diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee new file mode 100644 index 00000000000..c828e4b72f2 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-empty-view.coffee @@ -0,0 +1,12 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' +], ( + Marionette, + Templates +) -> + + class QualityGateSidebarListEmptyView extends Marionette.ItemView + tagName: 'li' + className: 'empty' + template: Templates['quality-gate-sidebar-list-empty'] diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee new file mode 100644 index 00000000000..70cd6d15366 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-item-view.coffee @@ -0,0 +1,27 @@ +define [ + 'backbone.marionette', + 'templates/quality-gates' +], ( + Marionette, + Templates +) -> + + class QualityGateSidebarListItemView extends Marionette.ItemView + tagName: 'li' + template: Templates['quality-gate-sidebar-list-item'] + + + modelEvents: + 'change': 'render' + + + events: + 'click': 'showQualityGate' + + + onRender: -> + @$el.toggleClass 'active', @options.highlighted + + + showQualityGate: -> + @options.app.router.navigate "show/#{@model.id}", trigger: true diff --git a/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee new file mode 100644 index 00000000000..43f92f74d27 --- /dev/null +++ b/sonar-server/src/main/coffee/quality-gate/views/quality-gate-sidebar-list-view.coffee @@ -0,0 +1,25 @@ +define [ + 'backbone.marionette', + 'quality-gate/views/quality-gate-sidebar-list-item-view', + 'quality-gate/views/quality-gate-sidebar-list-empty-view' +], ( + Marionette, + QualityGateSidebarListItemView, + QualityGateSidebarListEmptyView, +) -> + + class QualityGateSidebarListView extends Marionette.CollectionView + tagName: 'ol' + className: 'navigator-results-list' + itemView: QualityGateSidebarListItemView + emptyView: QualityGateSidebarListEmptyView + + + itemViewOptions: (model) -> + app: @options.app + highlighted: model.get('id') == +@highlighted + + + highlight: (id) -> + @highlighted = id + @render() diff --git a/sonar-server/src/main/coffee/tests/common/inputsSpec.coffee b/sonar-server/src/main/coffee/tests/common/inputsSpec.coffee new file mode 100644 index 00000000000..d20983d9c65 --- /dev/null +++ b/sonar-server/src/main/coffee/tests/common/inputsSpec.coffee @@ -0,0 +1,82 @@ +$ = jQuery + +describe 'WORK_DUR suite', -> + + beforeEach -> + window.SS = {} + window.SS.phrases = + 'work_duration': + 'x_days': '{0}d', 'x_hours': '{0}h', 'x_minutes': '{0}min' + + @input = $('') + @input.appendTo $('body') + @input.data 'type', 'WORK_DUR' + + + it 'converts', -> + @input.originalVal '2d 7h 13min' + expect(@input.val()).toBe 1393 + + + it 'converts only days', -> + @input.originalVal '1d' + expect(@input.val()).toBe 480 + + + it 'converts hours with minutes', -> + @input.originalVal '2h 30min' + expect(@input.val()).toBe 150 + + + it 'restores', -> + @input.val 1393 + expect(@input.originalVal()).toBe '2d 7h 13min' + + + it 'returns initially incorrect value', -> + @input.val 'something' + expect(@input.val()).toBe 'something' + + + +describe 'RATING suite', -> + + beforeEach -> + @input = $('') + @input.appendTo $('body') + @input.data 'type', 'RATING' + + + it 'converts A', -> + @input.originalVal 'A' + expect(@input.val()).toBe 1 + + + it 'converts B', -> + @input.originalVal 'B' + expect(@input.val()).toBe 2 + + + it 'converts E', -> + @input.originalVal 'E' + expect(@input.val()).toBe 5 + + + it 'does not convert F', -> + @input.originalVal 'F' + expect(@input.val()).toBe 'F' + + + it 'restores A', -> + @input.val 1 + expect(@input.originalVal()).toBe 'A' + + + it 'restores E', -> + @input.val 5 + expect(@input.originalVal()).toBe 'E' + + + it 'returns initially incorrect value', -> + @input.val 'something' + expect(@input.val()).toBe 'something' diff --git a/sonar-server/src/main/coffee/tests/translateSpec.coffee b/sonar-server/src/main/coffee/tests/translateSpec.coffee new file mode 100644 index 00000000000..7b4576bcfff --- /dev/null +++ b/sonar-server/src/main/coffee/tests/translateSpec.coffee @@ -0,0 +1,75 @@ +$ = jQuery + +describe 'translation "t" suite', -> + + beforeEach -> + window.messages = + 'something': 'SOMETHING' + 'something_with_underscore': 'SOMETHING_WITH_UNDERSCORE' + 'something_with{braces}': 'SOMETHING_WITH{braces}' + + window.SS = + phrases: + 'something': 'SOMETHING ANOTHER' + + + afterEach -> + window.messages = window.SS = undefined + + + it 'translates', -> + expect(t('something')).toBe 'SOMETHING' + + + it 'translates with underscore', -> + expect(t('something_with_underscore')).toBe 'SOMETHING_WITH_UNDERSCORE' + + + it 'translates with braces', -> + expect(t('something_with{braces}')).toBe 'SOMETHING_WITH{braces}' + + + it 'fallbacks to "translate"', -> + window.messages = undefined + expect(t('something')).toBe 'SOMETHING ANOTHER' + + + it 'returns the key when no translation', -> + expect(t('something_another')).toBe 'something_another' + + + +describe 'translation "translate" suite', -> + + beforeEach -> + window.SS = + phrases: + 'something': 'SOMETHING' + 'something_with_underscore': 'SOMETHING_WITH_UNDERSCORE' + 'something_with{braces}': 'SOMETHING_WITH{braces}' + + + afterEach -> + window.messages = window.SS = undefined + + + it 'translates', -> + expect(translate('something')).toBe 'SOMETHING' + + + it 'translates with underscore', -> + expect(translate('something_with_underscore')).toBe 'SOMETHING_WITH_UNDERSCORE' + + + it 'translates with braces', -> + expect(translate('something_with{braces}')).toBe 'SOMETHING_WITH{braces}' + + + it 'returns the key when no translation', -> + expect(translate('something_another')).toBe 'something_another' + + + it 'does not fail when there is no dictionary', -> + window.SS = undefined + expect(translate('something_another')).toBe 'something_another' + diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-actions.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-actions.hbs new file mode 100644 index 00000000000..ad149573ba4 --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-actions.hbs @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-bulk-change-dropdown.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-bulk-change-dropdown.hbs new file mode 100644 index 00000000000..7a6c1c98050 --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-bulk-change-dropdown.hbs @@ -0,0 +1,15 @@ + + {{t 'coding_rules.activate_in'}} + + + + {{t 'coding_rules.deactivate_in'}} + {{#if qualityProfile}}{{qualityProfileName}}{{/if}} + + +{{#if qualityProfile}} + + {{t 'coding_rules.change_severity_in'}} {{qualityProfileName}} + +{{/if}} \ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-bulk-change.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-bulk-change.hbs new file mode 100644 index 00000000000..0e3c9e7d54c --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-bulk-change.hbs @@ -0,0 +1,61 @@ +
+ + + + + +
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-detail-quality-profile.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-detail-quality-profile.hbs new file mode 100644 index 00000000000..b1633fcfaa5 --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-detail-quality-profile.hbs @@ -0,0 +1,54 @@ + + + + + + + + + + + + + {{#if note}} + + + + {{/if}} + +
+ {{name}} + {{#if parent}} +
+ {{parent.name}} +
+ {{/if}} +
+ {{severityIcon severity}} {{t "severity" severity}} + {{#if parent}}{{#notEq severity parent.severity}} +
+ {{t 'coding_rules.original'}} {{severityIcon parent.severity}}{{t 'severity' parent.severity}} +
+ {{/notEq}}{{/if}} +
+ {{#each parameters}} + {{key}}: {{value}} + {{#if ../parent}}{{#notEq value original}} +
+ {{t 'coding_rules.original'}} {{original}} +
+ {{/notEq}}{{/if}} + {{/each}} +
+
+ + {{#if parent}} + + {{/if}} + +
+
{{{note.html}}}
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-detail.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-detail.hbs new file mode 100644 index 00000000000..4b36a41182b --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-detail.hbs @@ -0,0 +1,151 @@ +{{#if contextQualityProfile}} +
+ + + + + + + + + +
+ {{contextQualityProfileName}} + {{#if qualityProfile}} + {{#with qualityProfile}} + + {{severityIcon severity}} {{t "severity" severity}} + + + {{#each parameters}} + + {{key}}: + {{value}} + + {{/each}} + + {{#if note}} + + {{{note.html}}} + + {{/if}} + {{/with}} + {{/if}} + +
+ {{#if qualityProfile}} + + {{#if parent}} + + {{/if}} + + {{else}} + + {{/if}} +
+
+
+{{/if}} + +

{{name}}

+ + + +
{{{description}}}
+ +
+
+ {{#if extra}} +
{{{extra}}}
{{/if}} +
+ +
+
+ +
+ + + + + + + + + + +
+ +
+ + {{t 'cancel'}} + + {{> '_markdown-tips' }} +
+
+ +
+ +
+
+ + +{{#if parameters}} +

{{t 'coding_rules.parameters'}}

+ +{{/if}} + + +
+

{{t 'coding_rules.quality_profiles'}}

+ +
+ +
+
+
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-facets-item.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-facets-item.hbs new file mode 100644 index 00000000000..634d28353c1 --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-facets-item.hbs @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-filter-bar.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-filter-bar.hbs new file mode 100644 index 00000000000..2e0f52a1b21 --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-filter-bar.hbs @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-header.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-header.hbs new file mode 100644 index 00000000000..37f1e638244 --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-header.hbs @@ -0,0 +1,5 @@ +

{{t 'coding_rules.page'}}

+ + \ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-layout.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-layout.hbs new file mode 100644 index 00000000000..98a5dd7c69a --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-layout.hbs @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-list-empty.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-list-empty.hbs new file mode 100644 index 00000000000..75e2daf75af --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-list-empty.hbs @@ -0,0 +1 @@ +{{t 'coding_rules.no_results'}} \ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-list-item.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-list-item.hbs new file mode 100644 index 00000000000..d44d28895da --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-list-item.hbs @@ -0,0 +1,5 @@ +
+ {{language}} +
{{#notEq status 'READY'}}{{status}}{{/notEq}}
+
+
{{name}}
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/coding-rules/coding-rules-quality-profile-activation.hbs b/sonar-server/src/main/hbs/coding-rules/coding-rules-quality-profile-activation.hbs new file mode 100644 index 00000000000..cfd8d6df01d --- /dev/null +++ b/sonar-server/src/main/hbs/coding-rules/coding-rules-quality-profile-activation.hbs @@ -0,0 +1,59 @@ +
+ + + + + +
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/common/_markdown-tips.hbs b/sonar-server/src/main/hbs/common/_markdown-tips.hbs new file mode 100644 index 00000000000..948a55e77ac --- /dev/null +++ b/sonar-server/src/main/hbs/common/_markdown-tips.hbs @@ -0,0 +1,4 @@ +
+ {{t 'markdown.helplink'}} : +   *{{t 'bold'}}*    ``{{t 'code'}}``    * {{t 'bulleted_point'}} +
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-actions.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-actions.hbs new file mode 100644 index 00000000000..efab29d2160 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-actions.hbs @@ -0,0 +1,6 @@ +

{{t 'quality_gates.page'}}

+{{#if canEdit}} + +{{/if}} \ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-condition.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-condition.hbs new file mode 100644 index 00000000000..572bdb2af79 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-condition.hbs @@ -0,0 +1,59 @@ + + {{metric.name}} + + + {{#if canEdit}} + + {{else}} + {{#if periodText}}Δ {{periodText}} + {{else}}{{t 'value'}} + {{/if}} + {{/if}} + + + {{#if canEdit}} + + {{else}} + {{t 'quality_gates.operator' op}} + {{/if}} + + + + {{#if canEdit}} + + {{else}} + {{warning}} + {{/if}} + + + + {{#if canEdit}} + + {{else}} + {{error}} + {{/if}} + + + {{#if canEdit}} + {{#if id}} +
+ + +
+ {{else}} +
+ + {{t 'cancel'}} +
+ {{/if}} + {{/if}} + \ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-conditions-empty.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-conditions-empty.hbs new file mode 100644 index 00000000000..40c92668496 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-conditions-empty.hbs @@ -0,0 +1,3 @@ + + {{t 'quality_gates.no_conditions'}} + \ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-conditions.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-conditions.hbs new file mode 100644 index 00000000000..343994fd780 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-conditions.hbs @@ -0,0 +1,43 @@ +
{{t 'quality_gates.conditions'}}
+ +
+

{{t 'quality_gates.introduction'}} + {{t 'more'}} +

+
+ {{t 'quality_gates.health_icons'}} +
    +
  • + + {{t 'alerts.notes.ok'}} +
  • +
  • + + {{t 'alerts.notes.warn'}} +
  • +
  • + + {{t 'alerts.notes.error'}} +
  • +
+
+
+ +{{#if canEdit}} +
+ + +
+{{/if}} + + + + +
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-header.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-header.hbs new file mode 100644 index 00000000000..e84a730d379 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-header.hbs @@ -0,0 +1,14 @@ +

{{name}}

+ +{{#if canEdit}} + +{{/if}} \ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-projects.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-projects.hbs new file mode 100644 index 00000000000..989cae0f31e --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail-projects.hbs @@ -0,0 +1,13 @@ +
{{t 'quality_gates.projects'}}
+ +{{#if default}} +

+ {{#if canEdit}} + {{t 'quality_gates.projects_for_default.edit'}} + {{else}} + {{t 'quality_gates.projects_for_default'}} + {{/if}} +

+{{else}} +
+{{/if}} \ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-detail.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail.hbs new file mode 100644 index 00000000000..0c5467b3476 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-detail.hbs @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-edit.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-edit.hbs new file mode 100644 index 00000000000..805d65ff307 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-edit.hbs @@ -0,0 +1,22 @@ +
+ + + + + +
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-sidebar-list-empty.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-sidebar-list-empty.hbs new file mode 100644 index 00000000000..8b017f09987 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-sidebar-list-empty.hbs @@ -0,0 +1 @@ +
{{t 'quality_gates.noQualityGates'}}
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gate-sidebar-list-item.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gate-sidebar-list-item.hbs new file mode 100644 index 00000000000..af8d93ae07b --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gate-sidebar-list-item.hbs @@ -0,0 +1 @@ +
{{name}} {{#if default}}({{t 'default'}}){{/if}}
\ No newline at end of file diff --git a/sonar-server/src/main/hbs/quality-gates/quality-gates-layout.hbs b/sonar-server/src/main/hbs/quality-gates/quality-gates-layout.hbs new file mode 100644 index 00000000000..32f6d7e2694 --- /dev/null +++ b/sonar-server/src/main/hbs/quality-gates/quality-gates-layout.hbs @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/sonar-server/src/main/js/application.js b/sonar-server/src/main/js/application.js new file mode 100644 index 00000000000..fbeef6d0c13 --- /dev/null +++ b/sonar-server/src/main/js/application.js @@ -0,0 +1,556 @@ +function showMessage(div_id, message) { + $j('#' + div_id + 'msg').html(message); + $j('#' + div_id).show(); +} +function error(message) { + showMessage('error', message); +} +function warning(message) { + showMessage('warning', message); +} +function info(message) { + showMessage('info', message); +} +function toggleFav(resourceId, elt) { + $j.ajax({type: 'POST', dataType: 'json', url: baseUrl + '/favourites/toggle/' + resourceId, + success: function (data) { + var star = $j(elt); + star.removeClass('fav notfav'); + star.addClass(data['css']); + star.attr('title', data['title']); + }}); +} + +function dashboardParameters() { + var queryString = window.location.search; + var parameters = ""; + + var matchDashboard = queryString.match(/did=\d+/); + if (matchDashboard && $j('#is-project-dashboard').length === 1) { + parameters += (matchDashboard[0] + "&"); + } + + var matchPeriod = queryString.match(/period=\d+/); + if (matchPeriod) { + // If we have a match for period, check that it is not project-specific + var period = parseInt(/period=(\d+)/.exec(queryString)[1]); + if (period <= 3) { + parameters += matchPeriod[0] + "&"; + } + } + + if (parameters !== "") { + parameters = "?" + parameters; + } + return parameters; +} + +function resourceViewerOnBulkIssues() { + var issuesTab = 'tab=issues'; + if (window.location.search.indexOf('tab=') >= 0) { + // If a tab is already selected + if (window.location.search.indexOf(issuesTab) >= 0) { + // If tab is issues, keep it and reload page + window.location.reload(); + } else { + // Else, switch to issues tab + window.location.search = window.location.search.replace(/tab=\w+/, issuesTab); + } + } else { + // No tab selected, see how to add tab parameter + if (window.location.search.startsWith('?')) { + window.location.search += ('&' + issuesTab); + } else { + window.location.search += ('?' + issuesTab); + } + } +} + +var SelectBox = { + cache: new Object(), + init: function (id) { + var box = document.getElementById(id); + var node; + SelectBox.cache[id] = []; + var cache = SelectBox.cache[id]; + for (var i = 0; (node = box.options[i]); i++) { + cache.push({value: node.value, text: node.text, displayed: 1}); + } + }, + redisplay: function (id) { + // Repopulate HTML select box from cache + var box = document.getElementById(id); + // clear all options + box.options.length = 0; + for (var i = 0, j = SelectBox.cache[id].length; i < j; i++) { + var node = SelectBox.cache[id][i]; + if (node.displayed) { + box.options[box.options.length] = new Option(node.text, node.value, false, false); + } + } + }, + filter: function (id, text) { + // Redisplay the HTML select box, displaying only the choices containing ALL + // the words in text. (It's an AND search.) + var tokens = text.toLowerCase().split(/\s+/); + var node, token; + for (var i = 0; (node = SelectBox.cache[id][i]); i++) { + node.displayed = 1; + for (var j = 0; (token = tokens[j]); j++) { + if (node.text.toLowerCase().indexOf(token) == -1) { + node.displayed = 0; + } + } + } + SelectBox.redisplay(id); + }, + delete_from_cache: function (id, value) { + var node, delete_index = null; + for (var i = 0; (node = SelectBox.cache[id][i]); i++) { + if (node.value == value) { + delete_index = i; + break; + } + } + var j = SelectBox.cache[id].length - 1; + for (var i = delete_index; i < j; i++) { + SelectBox.cache[id][i] = SelectBox.cache[id][i + 1]; + } + SelectBox.cache[id].length--; + }, + add_to_cache: function (id, option) { + SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1}); + }, + cache_contains: function (id, value) { + // Check if an item is contained in the cache + var node; + for (var i = 0; (node = SelectBox.cache[id][i]); i++) { + if (node.value == value) { + return true; + } + } + return false; + }, + move: function (from, to) { + var from_box = document.getElementById(from); + var option; + for (var i = 0; (option = from_box.options[i]); i++) { + if (option.selected && SelectBox.cache_contains(from, option.value)) { + SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option.value); + } + } + SelectBox.redisplay(from); + SelectBox.redisplay(to); + }, + move_all: function (from, to) { + var from_box = document.getElementById(from); + var option; + for (var i = 0; (option = from_box.options[i]); i++) { + if (SelectBox.cache_contains(from, option.value)) { + SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1}); + SelectBox.delete_from_cache(from, option.value); + } + } + SelectBox.redisplay(from); + SelectBox.redisplay(to); + }, + sort: function (id) { + SelectBox.cache[id].sort(function (a, b) { + a = a.text.toLowerCase(); + b = b.text.toLowerCase(); + try { + if (a > b) { + return 1; + } + if (a < b) { + return -1; + } + } + catch (e) { + // silently fail on IE 'unknown' exception + } + return 0; + }); + }, + select_all: function (id) { + var box = document.getElementById(id); + for (var i = 0; i < box.options.length; i++) { + box.options[i].selected = 'selected'; + } + } +}; + + +var treemaps = {}; + +function treemapById(id) { + return treemaps[id]; +} +var TreemapContext = function (rid, label) { + this.rid = rid; + this.label = label; +}; + +/** + * HTML elements : + * tm-#{id} : required treemap container + * tm-bc-#{id} : required breadcrumb + * tm-loading-#{id} : optional loading icon + */ +var Treemap = function (id, sizeMetric, colorMetric, heightPercents) { + this.id = id; + this.sizeMetric = sizeMetric; + this.colorMetric = colorMetric; + this.breadcrumb = []; + treemaps[id] = this; + this.rootNode().height(this.rootNode().width() * heightPercents / 100); + this.initNodes(); + +}; +Treemap.prototype.currentContext = function () { + if (this.breadcrumb.length > 0) { + return this.breadcrumb[this.breadcrumb.length - 1]; + } + return null; +}; +Treemap.prototype.load = function () { + var context = this.currentContext(); + var output = ''; + this.breadcrumb.forEach(function (ctx) { + output += ctx.label + ' / '; + }); + $j('#tm-bc-' + this.id).html(output); + $j('#tm-loading-' + this.id).show(); + var self = this; + $j.ajax({ + type: "GET", + url: baseUrl + '/treemap/index?html_id=' + this.id + '&size_metric=' + this.sizeMetric + '&color_metric=' + this.colorMetric + '&resource=' + context.rid, + dataType: "html", + success: function (data) { + if (data.length > 1) { + self.rootNode().html(data); + self.initNodes(); + } else { + // SONAR-3524 + // When data is empty, do not display it, revert breadcrumb state and display a message to user + self.breadcrumb.pop(); + $j("#tm-bottom-level-reached-msg-" + self.id).show(); + } + $j("#tm-loading-" + self.id).hide(); + } + }); +}; +Treemap.prototype.rootNode = function () { + return $j('#tm-' + this.id); +}; + +Treemap.prototype.initNodes = function () { + var self = this; + $j('#tm-' + this.id).find('a').each(function (index) { + $j(this).on("click", function (event) { + event.stopPropagation(); + }); + }); + $j('#tm-' + this.id).find('[rid]').each(function (index) { + $j(this).on("contextmenu", function (event) { + event.stopPropagation(); + event.preventDefault(); + $j("#tm-bottom-level-reached-msg-" + self.id).hide(); + // right click + if (self.breadcrumb.length > 1) { + self.breadcrumb.pop(); + self.load(); + } else if (self.breadcrumb.length == 1) { + $j("#tm-loading-" + self.id).show(); + location.reload(); + } + return false; + }); + $j(this).on("click", function (event) { + var source = $j(this); + var rid = source.attr('rid'); + var context = new TreemapContext(rid, source.text()); + self.breadcrumb.push(context); + self.load(); + } + ); + }); +}; + +function openModalWindow(url, options) { + var width = (options && options['width']) || 540; + var $dialog = $j('#modal'); + if (!$dialog.length) { + $dialog = $j('').appendTo('body'); + } + $j.get(url,function (html) { + $dialog.removeClass('ui-widget-overlay'); + $dialog.html(html); + $dialog + .dialog({ + dialogClass: "no-close", + width: width, + draggable: false, + autoOpen: false, + modal: true, + minHeight: 50, + resizable: false, + title: null, + close: function () { + $j('#modal').remove(); + } + }); + $dialog.dialog("open"); + }).fail(function () { + alert("Server error. Please contact your administrator."); + }).always(function () { + $dialog.removeClass('ui-widget-overlay'); + }); + return false; +} + +(function ($j) { + $j.fn.extend({ + openModal: function() { + return this.each(function () { + var obj = $j(this); + var url = obj.attr('modal-url') || obj.attr('href'); + return openModalWindow(url, {'width': obj.attr('modal-width')}); + }); + }, + modal: function () { + return this.each(function () { + var obj = $j(this); + obj.unbind('click'); + var $link = obj.bind('click', function () { + $link.openModal(); + return false; + }); + }); + }, + modalForm: function (ajax_options) { + return this.each(function () { + var obj = $j(this); + obj.submit(function (event) { + $j('input[type=submit]', this).attr('disabled', 'disabled'); + $j.ajax($j.extend({ + type: 'POST', + url: obj.attr('action'), + data: obj.serialize(), + success: function (data) { + window.location.reload(); + }, + error: function (xhr, textStatus, errorThrown) { + // If the modal window has defined a modal-error element, then returned text must be displayed in it + var errorElt = obj.find(".modal-error"); + if (errorElt.length) { + // Hide all loading images + $j('.loading-image').addClass("hidden"); + // Re activate submit button + $j('input[type=submit]', obj).removeAttr('disabled'); + errorElt.show(); + errorElt.html(xhr.responseText); + } else { + // otherwise replace modal window by the returned text + $j("#modal").html(xhr.responseText); + } + } + }, ajax_options)); + return false; + }); + }); + } + }); +})(jQuery); + +function closeModalWindow() { + $j('#modal').dialog('close'); + return false; +} + +function supports_html5_storage() { + try { + return 'localStorage' in window && window['localStorage'] !== null; + } catch (e) { + return false; + } +} + +//******************* HANDLING OF ACCORDION NAVIGATION [BEGIN] ******************* // + +function openAccordionItem(url, elt, updateCurrentElement) { + var htmlClass = 'accordion-item'; + var currentElement = $j(elt).closest('.'+ htmlClass); + + // Create loading image + var loadingImg = new Image(); + loadingImg.src = baseUrl + "/images/loading.gif"; + loadingImg.className = 'accordion-loading'; + var loading = $j(loadingImg); + var existingLoading = currentElement.find('.accordion-loading'); + if (updateCurrentElement && existingLoading.length) { + existingLoading.show(); + loading.hide(); + } + + // Remove elements under current element + if (currentElement.length) { + // Fix the height in order to not change the position on the screen when removing elements under current element + var elementToRemove = currentElement.nextAll('.'+ htmlClass); + if (elementToRemove.height()) { + $j("#accordion-panel").height($j("#accordion-panel").height() + elementToRemove.height()); + } + // Remove all accordion items after current element + elementToRemove.remove(); + // Display loading image only if not already displayed (if previous call was not finished) + if (currentElement.next('.accordion-loading').length == 0) { + loading.insertAfter(currentElement); + } + } else { + // Current element is not in a working view, remove all working views + $j('.'+ htmlClass).remove(); + // Display loading image only if not already displayed (if previous call was not finished) + if ($j("#accordion-panel").next('.accordion-loading').length == 0) { + loading.insertAfter($j("#accordion-panel")); + } + } + + // Get content from url + var ajaxRequest = $j.ajax({ + url: url + }).fail(function (jqXHR, textStatus) { + var error = "Server error. Please contact your administrator. The status of the error is : "+ jqXHR.status + ", textStatus is : "+ textStatus; + console.log(error); + $j("#accordion-panel").append($j('
').append(error)); + }).done(function (html) { + if (currentElement.length) { + var body = currentElement.find('.accordion-item-body'); + if (!updateCurrentElement && !body.hasClass('accordion-item-body-medium')) { + body.addClass("accordion-item-body-medium"); + elt.scrollIntoView(false); + } + } else { + $j("#accordion-panel").height('auto'); + + // Current element is not in a working view, remove again all working views to purge elements that could be added just before this one + $j('.'+ htmlClass).remove(); + } + + if (updateCurrentElement) { + // Fix the height in order to not change the position on the screen + var prevHeight = $j("#accordion-panel").height(); + currentElement.html(html); + $j("#accordion-panel").height('auto'); + var newHeight = $j("#accordion-panel").height(); + if (prevHeight > newHeight) { + $j("#accordion-panel").height(prevHeight); + } else { + $j("#accordion-panel").height(newHeight); + } + } else { + // Add new item add the end of the panel and restore the height param + var nbElement = $j("."+htmlClass).size(); + var newElement = $j('
'); + $j("#accordion-panel").append(newElement); + + // Add html after having adding the new element in the page in order to scripts (for instance for GWT) to be well executed + newElement.append(html); + $j("#accordion-panel").height('auto'); + + // Set the focus on the top of the current item with animation + if (currentElement.length) { + $j('html, body').animate({ + scrollTop: currentElement.offset().top}, 500 + ); + } + } + loading.remove(); + }); + return ajaxRequest; +} + + +function expandAccordionItem(elt) { + var currentElement = $j(elt).closest('.accordion-item'); + currentElement.find('.accordion-item-body').removeClass("accordion-item-body-medium"); +} + +//******************* HANDLING OF ACCORDION NAVIGATION [END] ******************* // + + +//******************* HANDLING OF DROPDOWN MENUS [BEGIN] ******************* // + +var currentlyDisplayedDropdownMenu; + +var hideCurrentDropdownMenu = function () { + if (currentlyDisplayedDropdownMenu) { + currentlyDisplayedDropdownMenu.hide(); + } + $j(document).unbind('mouseup', hideCurrentDropdownMenu); +}; + +var clickOnDropdownMenuLink = function (event) { + var link = $j(event.target).children('a'); + if (link) { + var href = link.attr('href'); + if (href && href.length > 1) { + // there's a real link, not a href="#" + window.location = href; + } else { + // otherwise, this means that the link is handled with an onclick event (for Ajax calls) + link.click(); + } + } +}; + +function showDropdownMenu(menuId) { + showDropdownMenuOnElement($j('#' + menuId)); +} + +function showDropdownMenuOnElement(elt) { + var dropdownElt = $j(elt); + + if (dropdownElt == currentlyDisplayedDropdownMenu) { + currentlyDisplayedDropdownMenu = ""; + } else { + currentlyDisplayedDropdownMenu = dropdownElt; + $j(document).mouseup(hideCurrentDropdownMenu); + + var dropdownChildren = dropdownElt.find('li'); + dropdownChildren.unbind('click'); + dropdownChildren.click(clickOnDropdownMenuLink); + dropdownElt.show(); + } +} + +//******************* HANDLING OF DROPDOWN MENUS [END] ******************* // + +function openPopup(url, popupId) { + window.open(url,popupId,'height=800,width=900,scrollbars=1,resizable=1'); + return false; +} + + +jQuery(function() { + + // Initialize top search + jQuery('#searchInput').topSearch({ + minLength: 2, + results: '#searchResourcesResults', + spinner: '#searchingResources' + }); + + + // Process login link in order to add the anchor + jQuery('#login-link').on('click', function(e) { + e.preventDefault(); + var href = jQuery(this).prop('href'), + hash = window.location.hash; + if (hash.length > 0) { + href += decodeURIComponent(hash); + } + window.location = href; + }); +}); + diff --git a/sonar-server/src/main/js/common/handlebars-extensions.js b/sonar-server/src/main/js/common/handlebars-extensions.js new file mode 100644 index 00000000000..07c1044a316 --- /dev/null +++ b/sonar-server/src/main/js/common/handlebars-extensions.js @@ -0,0 +1,163 @@ +define(['handlebars'], function (Handlebars) { + + /* + * Shortcut for templates retrieving + */ + window.getTemplate = function(templateSelector) { + return Handlebars.compile(jQuery(templateSelector).html() || ''); + }; + + var defaultActions = ['comment', 'assign', 'assign_to_me', 'plan', 'set_severity']; + + Handlebars.registerHelper('capitalize', function(string) { + return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); + }); + + Handlebars.registerHelper('severityIcon', function(severity) { + return new Handlebars.SafeString( + '' + ); + }); + + Handlebars.registerHelper('statusIcon', function(status) { + return new Handlebars.SafeString( + '' + ); + }); + + Handlebars.registerHelper('resolutionIcon', function(resolution) { + return new Handlebars.SafeString( + '' + ); + }); + + Handlebars.registerHelper('eq', function(v1, v2, options) { + return v1 == v2 ? options.fn(this) : options.inverse(this); + }); + + Handlebars.registerHelper('notEq', function(v1, v2, options) { + return v1 != v2 ? options.fn(this) : options.inverse(this); + }); + + Handlebars.registerHelper('all', function() { + var args = Array.prototype.slice.call(arguments, 0, -1), + options = arguments[arguments.length - 1], + all = args.reduce(function(prev, current) { + return prev && current; + }, true); + return all ? options.fn(this) : options.inverse(this); + }); + + Handlebars.registerHelper('any', function() { + var args = Array.prototype.slice.call(arguments, 0, -1), + options = arguments[arguments.length - 1], + all = args.reduce(function(prev, current) { + return prev || current; + }, true); + return all ? options.fn(this) : options.inverse(this); + }); + + Handlebars.registerHelper('inArray', function(array, element, options) { + if (array.indexOf(element) !== -1) { + return options.fn(this); + } else { + return options.inverse(this); + } + }); + + Handlebars.registerHelper('ifNotEmpty', function() { + var args = Array.prototype.slice.call(arguments, 0, -1), + options = arguments[arguments.length - 1], + notEmpty = args.reduce(function(prev, current) { + return prev || (current && current.length > 0); + }, false); + return notEmpty ? options.fn(this) : ''; + }); + + Handlebars.registerHelper('join', function(array, separator) { + return array.join(separator); + }); + + Handlebars.registerHelper('dashboardUrl', function(componentKey, componentQualifier) { + var url = '/dashboard/index/' + decodeURIComponent(componentKey); + if (componentQualifier === 'FIL' || componentQualifier === 'CLA') { + url += '?metric=sqale_index'; + } + return url; + }); + + Handlebars.registerHelper('translate', function() { + var args = Array.prototype.slice.call(arguments, 0, -1); + return window.translate.apply(this, args); + }); + + Handlebars.registerHelper('t', function() { + var args = Array.prototype.slice.call(arguments, 0, -1); + return window.t.apply(this, args); + }); + + Handlebars.registerHelper('pluginActions', function(actions, options) { + var pluginActions = _.difference(actions, defaultActions); + return pluginActions.reduce(function(prev, current) { + return prev + options.fn(current); + }, ''); + }); + + Handlebars.registerHelper('ifHasExtraTransitions', function(transitions, options) { + if (transitions && transitions.length > 1) { + return options.fn(this); + } else { + return ''; + } + }); + + Handlebars.registerHelper('ifHasExtraActions', function(actions, options) { + var actionsLeft = _.difference(actions, _.without(defaultActions, 'set_severity')); + if (actionsLeft.length > 0) { + return options.fn(this); + } else { + return ''; + } + }); + + Handlebars.registerHelper('withFirst', function(list, options) { + if (list && list.length > 0) { + return options.fn(list[0]); + } else { + return ''; + } + }); + + Handlebars.registerHelper('withoutFirst', function(list, options) { + if (list && list.length > 1) { + return list.slice(1).reduce(function(prev, current) { + return prev + options.fn(current); + }, ''); + } else { + return ''; + } + }); + + Handlebars.registerHelper('sources', function(source, scm, options) { + var sources = _.map(source, function(code, line) { + return { + lineNumber: line, + code: code, + scm: (scm && scm[line]) ? { author: scm[line][0], date: scm[line][1] } : undefined + } + }); + + return sources.reduce(function(prev, current, index) { + return prev + options.fn(_.extend({ first: index === 0 }, current)); + }, ''); + }); + + Handlebars.registerHelper('operators', function(metricType, options) { + var ops = ['LT', 'GT', 'EQ', 'NE']; + + return ops.reduce(function(prev, current) { + return prev + options.fn(current); + }, ''); + }); + +}); diff --git a/sonar-server/src/main/js/common/select-list.js b/sonar-server/src/main/js/common/select-list.js new file mode 100644 index 00000000000..8d91dc39dca --- /dev/null +++ b/sonar-server/src/main/js/common/select-list.js @@ -0,0 +1,428 @@ +requirejs.config({ + baseUrl: baseUrl + '/js', + + paths: { + 'backbone': 'third-party/backbone' + }, + + shim: { + 'backbone': { + exports: 'Backbone' + } + } + +}); + +requirejs(['backbone'], function (Backbone) { + + (function ($) { + + var showError = null; + + /* + * SelectList Collection + */ + + var SelectListCollection = Backbone.Collection.extend({ + + parse: function (r) { + this.more = r.more; + return r.results; + }, + + fetch: function (options) { + var data = $.extend({ + page: 1, + pageSize: 100 + }, options.data || {}), + settings = $.extend({}, options, { data: data }); + + this.settings = { + url: settings.url, + data: data + }; + + Backbone.Collection.prototype.fetch.call(this, settings); + }, + + fetchNextPage: function (options) { + if (this.more) { + var nextPage = this.settings.data.page + 1, + settings = $.extend(this.settings, options); + + settings.data.page = nextPage; + + this.fetch(settings); + } + } + + }); + + + /* + * SelectList Item View + */ + + var SelectListItemView = Backbone.View.extend({ + tagName: 'li', + + template: function (d) { + return '' + + '
' + d + '
'; + }, + + events: { + 'change .select-list-list-checkbox': 'toggle' + }, + + initialize: function (options) { + this.listenTo(this.model, 'change', this.render); + this.settings = options.settings; + }, + + render: function () { + this.$el.html(this.template(this.settings.format(this.model.toJSON()))); + this.$('input').prop('name', this.model.get('name')); + this.$el.toggleClass('selected', this.model.get('selected')); + this.$('.select-list-list-checkbox') + .prop('title', + this.model.get('selected') ? + this.settings.tooltips.deselect : + this.settings.tooltips.select) + .prop('checked', this.model.get('selected')); + + if (this.settings.readOnly) { + this.$('.select-list-list-checkbox').prop('disabled', true); + } + }, + + remove: function (postpone) { + if (postpone) { + var that = this; + that.$el.addClass(this.model.get('selected') ? 'added' : 'removed'); + setTimeout(function () { + Backbone.View.prototype.remove.call(that, arguments); + }, 500); + } else { + Backbone.View.prototype.remove.call(this, arguments); + } + }, + + toggle: function (e) { + var selected = this.model.get('selected'), + that = this, + url = selected ? this.settings.deselectUrl : this.settings.selectUrl, + data = $.extend({}, this.settings.extra || {}); + + data[this.settings.selectParameter] = this.model.get(this.settings.selectParameterValue); + + that.$el.addClass('progress'); + $.ajax({ + url: url, + type: 'POST', + data: data + }) + .done(function () { + that.model.set('selected', !selected); + }) + .fail(showError) + .always(function () { + that.$el.removeClass('progress'); + }); + } + }); + + + /* + * SelectList View + */ + + var SelectListView = Backbone.View.extend({ + template: function (l) { + return '
' + + '
' + + '' + + '
' + + '' + + '×' + + '
' + + '
' + + '
' + + '
    ' + + '
    ' + + '
    '; + }, + + events: { + 'click .select-list-control-button[name=selected]': 'showSelected', + 'click .select-list-control-button[name=deselected]': 'showDeselected', + 'click .select-list-control-button[name=all]': 'showAll', + + 'click .select-list-search-control-clear': 'clearSearch' + }, + + initialize: function (options) { + this.listenTo(this.collection, 'add', this.renderListItem); + this.listenTo(this.collection, 'reset', this.renderList); + this.listenTo(this.collection, 'remove', this.removeModel); + this.listenTo(this.collection, 'change:selected', this.confirmFilter); + this.settings = options.settings; + }, + + render: function () { + var that = this, + keyup = function () { + that.search(); + }; + + this.$el.html(this.template(this.settings.labels)) + .width(this.settings.width); + + this.$listContainer = this.$('.select-list-list-container'); + if (!this.settings.readOnly) { + this.$listContainer + .height(this.settings.height) + .css('overflow', 'auto') + .on('scroll', function () { + that.scroll(); + }); + } else { + this.$listContainer.addClass('select-list-list-container-readonly'); + } + + this.$list = this.$('.select-list-list'); + + var searchInput = this.$('.select-list-search-control input') + .on('keyup', _.debounce(keyup, 250)); + + setTimeout(function () { + searchInput.focus(); + }, 250); + + this.listItemViews = []; + + showError = function () { + $('
    ') + .addClass('error').text(that.settings.errorMessage) + .insertBefore(that.$el); + }; + + if (this.settings.readOnly) { + this.$('.select-list-control').remove(); + } + }, + + renderList: function () { + this.listItemViews.forEach(function (view) { + view.remove(); + }); + this.listItemViews = []; + if (this.collection.length > 0) { + this.collection.each(this.renderListItem, this); + } else { + if (this.settings.readOnly) { + this.renderEmpty(); + } + } + this.$listContainer.scrollTop(0); + }, + + renderListItem: function (item) { + var itemView = new SelectListItemView({ + model: item, + settings: this.settings + }); + this.listItemViews.push(itemView); + this.$list.append(itemView.el); + itemView.render(); + }, + + renderEmpty: function () { + this.$list.append('
  • ' + this.settings.labels.noResults + '
  • '); + }, + + confirmFilter: function (model) { + if (this.currentFilter !== 'all') { + this.collection.remove(model); + } + }, + + removeModel: function (model, collection, options) { + this.listItemViews[options.index].remove(true); + this.listItemViews.splice(options.index, 1); + }, + + filterBySelection: function (filter) { + var that = this; + filter = this.currentFilter = filter || this.currentFilter; + + if (filter != null) { + this.$('.select-list-check-control').toggleClass('disabled', false); + this.$('.select-list-search-control').toggleClass('disabled', true); + this.$('.select-list-search-control input').val(''); + + this.$('.select-list-control-button').removeClass('active') + .filter('[name=' + filter + ']').addClass('active'); + + this.showFetchSpinner(); + + this.collection.fetch({ + url: this.settings.searchUrl, + reset: true, + data: { selected: filter }, + success: function () { + that.hideFetchSpinner(); + }, + error: showError + }); + } + }, + + showSelected: function () { + this.filterBySelection('selected'); + }, + + showDeselected: function () { + this.filterBySelection('deselected'); + }, + + showAll: function () { + this.filterBySelection('all'); + }, + + search: function () { + var query = this.$('.select-list-search-control input').val(), + hasQuery = query.length > 0, + that = this; + + this.$('.select-list-check-control').toggleClass('disabled', hasQuery); + this.$('.select-list-search-control').toggleClass('disabled', !hasQuery); + + if (hasQuery) { + this.showFetchSpinner(); + this.currentFilter = 'all'; + + this.collection.fetch({ + url: this.settings.searchUrl, + reset: true, + data: { query: query }, + success: function () { + that.hideFetchSpinner(); + }, + error: showError + }); + } else { + this.filterBySelection(); + } + }, + + searchByQuery: function (query) { + this.$('.select-list-search-control input').val(query); + this.search(); + }, + + clearSearch: function () { + this.filterBySelection(); + }, + + showFetchSpinner: function () { + this.$listContainer.addClass('loading'); + }, + + hideFetchSpinner: function () { + this.$listContainer.removeClass('loading'); + }, + + scroll: function () { + var scrollBottom = this.$listContainer.scrollTop() >= + this.$list[0].scrollHeight - this.$listContainer.outerHeight(), + that = this; + + if (scrollBottom && this.collection.more) { + $.throttle(250, function () { + that.showFetchSpinner(); + + that.collection.fetchNextPage({ + success: function () { + that.hideFetchSpinner(); + } + }); + })(); + } + } + + }); + + + /* + * SelectList Entry Point + */ + + window.SelectList = function (options) { + this.settings = $.extend(window.SelectList.defaults, options); + + this.collection = new SelectListCollection(); + + this.view = new SelectListView({ + el: this.settings.el, + collection: this.collection, + settings: this.settings + }); + + this.view.render(); + this.filter('selected'); + return this; + }; + + + /* + * SelectList API Methods + */ + + window.SelectList.prototype.filter = function (filter) { + this.view.filterBySelection(filter); + return this; + }; + + window.SelectList.prototype.search = function (query) { + this.view.searchByQuery(query); + return this; + }; + + + /* + * SelectList Defaults + */ + + window.SelectList.defaults = { + width: '50%', + height: 400, + + readOnly: false, + + format: function (item) { + return item.value; + }, + + labels: { + selected: 'Selected', + deselected: 'Deselected', + all: 'All', + noResults: '' + }, + + tooltips: { + select: 'Click this to select item', + deselect: 'Click this to deselect item' + }, + + errorMessage: 'Something gone wrong, try to reload the page and try again.' + }; + + })(jQuery); + +}); diff --git a/sonar-server/src/main/js/dashboard.js b/sonar-server/src/main/js/dashboard.js new file mode 100644 index 00000000000..576dba7046d --- /dev/null +++ b/sonar-server/src/main/js/dashboard.js @@ -0,0 +1,134 @@ +(function($) { + + window.Portal = function(options) { + this.initialize(options); + }; + + window.Portal.prototype = { + + initialize: function(options) { + this.options = options; + if (!this.options.editorEnabled) { + return; + } + this.createAllSortables(); + this.lastSaveString = ''; + this.saveDashboardsState(); + }, + + + createAllSortables: function() { + var that = this, + blocks = $('.' + this.options.block), + columnHandle = $('.' + this.options.columnHandle), + draggable, + + onDragLeave = function(e) { + $(e.currentTarget).removeClass(that.options.hoverClass); + }, + + onDrop = function(e) { + e.preventDefault(); + draggable.detach().insertBefore($(e.currentTarget)); + onDragLeave(e); + that.saveDashboardsState(); + }; + + blocks + .prop('draggable', true) + .on('dragstart', function(e) { + e.originalEvent.dataTransfer.setData('text/plain', 'drag'); + draggable = $(this); + columnHandle.show(); + }) + .on('dragover', function(e) { + if (draggable.prop('id') !== $(this).prop('id')) { + e.preventDefault(); + $(e.currentTarget).addClass(that.options.hoverClass); + } + }) + .on('drop', onDrop) + .on('dragleave', onDragLeave); + + columnHandle + .on('dragover', function(e) { + e.preventDefault(); + $(e.currentTarget).addClass(that.options.hoverClass); + }) + .on('drop', onDrop) + .on('dragleave', onDragLeave); + }, + + + highlightWidget: function(widgetId) { + var block = $('#block_' + widgetId), + options = this.options; + block.css('background-color', options.highlightStartColor); + setTimeout(function() { + block.css('background-color', options.highlightEndColor); + }, this.options.highlightDuration); + }, + + + saveDashboardsState: function() { + var options = this.options, + result = $('.' + this.options.column).map(function () { + var blocks = $(this).find('.' + options.block); + $(this).find('.' + options.columnHandle).toggle(blocks.length === 0); + + return blocks.map(function () { + return $(this).prop('id').substring(options.block.length + 1); + }).get().join(','); + }).get().join(';'); + + if (result === this.lastSaveString) { + return; + } + + var firstTime = this.lastSaveString === ''; + this.lastSaveString = result; + + if (firstTime) { + return; + } + + if (this.options.saveUrl) { + var postBody = this.options.dashboardState + '=' + escape(result); + + $.ajax({ + url: this.options.saveUrl, + type: 'POST', + data: postBody + }); + } + }, + + + editWidget: function(widgetId) { + $('#widget_title_' + widgetId).hide(); + $('#widget_' + widgetId).hide(); + $('#widget_props_' + widgetId).show(); + }, + + + cancelEditWidget: function(widgetId) { + $('widget_title_' + widgetId).show(); + $('#widget_' + widgetId).show(); + $('#widget_props_' + widgetId).hide(); + }, + + + deleteWidget: function(element) { + $(element).closest('.' + this.options.block).remove(); + this.saveDashboardsState(); + } + }; + + + + window.autoResize = function(everyMs, callback) { + var debounce = _.debounce(callback, everyMs); + $(window).on('resize', debounce); + }; + +})(jQuery); diff --git a/sonar-server/src/main/js/duplication.js b/sonar-server/src/main/js/duplication.js new file mode 100644 index 00000000000..edd28f41fee --- /dev/null +++ b/sonar-server/src/main/js/duplication.js @@ -0,0 +1,21 @@ +// JS scripts used in the duplication tab of the resource viewer + +function updateDuplicationLines(url, groupId, itemId, linesCount, fromLine, toLine) { + $j('#duplGroup_' + groupId + ' p.selected').removeClass('selected'); + $j('#duplCount-' + groupId + '-' + itemId).addClass('selected'); + $j('#duplFrom-' + groupId + '-' + itemId).addClass('selected'); + $j('#duplName-' + groupId + '-' + itemId).addClass('selected'); + $j('#duplLoading-' + groupId).addClass('loading'); + + if ($j('#source-' + groupId+ ' :first-child').hasClass('expanded')) { + toLine = fromLine + linesCount - 1; + } + $j.ajax({ + url: url + "&to_line=" + toLine + "&from_line=" + fromLine + "&lines_count=" + linesCount + "&group_index=" + groupId, + success:function(response){ + $j('#source-' + groupId).html(response); + }, + type:'get' + }); + return false; +} \ No newline at end of file diff --git a/sonar-server/src/main/js/issue.js b/sonar-server/src/main/js/issue.js new file mode 100644 index 00000000000..3a91c7401ba --- /dev/null +++ b/sonar-server/src/main/js/issue.js @@ -0,0 +1,250 @@ +/* Open form for most common actions like comment, assign or plan */ +function issueForm(actionType, elt) { + var issueElt = $j(elt).closest('[data-issue-key]'); + var issueKey = issueElt.attr('data-issue-key'); + var actionsElt = issueElt.find('.code-issue-actions'); + var formElt = issueElt.find('.code-issue-form'); + + actionsElt.addClass('hidden'); + formElt.html("").removeClass('hidden'); + + $j.ajax(baseUrl + "/issue/action_form/" + actionType + "?issue=" + issueKey) + .done(function (msg) { + formElt.html(msg); + var focusField = formElt.find('[autofocus]'); + if (focusField != null) { + focusField.focus(); + } + }) + .fail(function (jqXHR, textStatus) { + alert(textStatus); + }); + return false; +} + +/* Close forms opened through the method issueForm() */ +function closeIssueForm(elt) { + var issueElt = $j(elt).closest('[data-issue-key]'); + var actionsElt = issueElt.find('.code-issue-actions'); + var formElt = issueElt.find('.code-issue-form'); + + formElt.addClass('hidden'); + actionsElt.removeClass('hidden'); + return false; +} + +/* Raise a Javascript event for Eclipse Web View */ +function notifyIssueChange(issueKey) { + $j(document).trigger('sonar.issue.updated', [issueKey]); +} + +/* Submit forms opened through the method issueForm() */ +function submitIssueForm(elt) { + var formElt = $j(elt).closest('form'); + formElt.find('.loading').removeClass('hidden'); + formElt.find(':submit').prop('disabled', true); + var issueElt = formElt.closest('[data-issue-key]'); + var issueKey = issueElt.attr('data-issue-key'); + + $j.ajax({ + type: "POST", + url: baseUrl + '/issue/do_action', + data: formElt.serialize()} + ).success(function (htmlResponse) { + var replaced = $j(htmlResponse); + issueElt.replaceWith(replaced); + notifyIssueChange(issueKey); + } + ).fail(function (jqXHR, textStatus) { + closeIssueForm(elt); + issueElt.find('.code-issue-actions').replaceWith(jqXHR.responseText); + }); + return false; +} + +function doIssueAction(elt, action, parameters) { + var issueElt = $j(elt).closest('[data-issue-key]'); + var issueKey = issueElt.attr('data-issue-key'); + + issueElt.find('.code-issue-actions').html(""); + parameters['issue'] = issueKey; + + $j.ajax({ + type: "POST", + url: baseUrl + '/issue/do_action/' + action, + data: parameters + } + ).success(function (htmlResponse) { + var replaced = $j(htmlResponse); + issueElt.replaceWith(replaced); + notifyIssueChange(issueKey); + } + ).fail(function (jqXHR, textStatus) { + issueElt.find('.code-issue-actions').replaceWith(jqXHR.responseText); + }); + return false; +} + +// Used for actions defined by plugins +function doPluginIssueAction(elt, action) { + var parameters = {}; + return doIssueAction(elt, action, parameters); +} + +function assignIssueToMe(elt) { + var parameters = {'me': true}; + return doIssueAction(elt, 'assign', parameters); +} + +function doIssueTransition(elt, transition) { + var parameters = {'transition': transition}; + return doIssueAction(elt, 'transition', parameters); +} + +function deleteIssueComment(elt, confirmMsg) { + var commentElt = $j(elt).closest("[data-comment-key]"); + var commentKey = commentElt.attr('data-comment-key'); + var issueElt = commentElt.closest('[data-issue-key]'); + if (confirm(confirmMsg)) { + $j.ajax({ + type: "POST", + url: baseUrl + "/issue/delete_comment?id=" + commentKey, + success: function (htmlResponse) { + issueElt.replaceWith($j(htmlResponse)); + } + }); + } + return false; +} + +function formEditIssueComment(elt) { + var commentElt = $j(elt).closest("[data-comment-key]"); + var commentKey = commentElt.attr('data-comment-key'); + var issueElt = commentElt.closest('[data-issue-key]'); + + issueElt.find('.code-issue-actions').addClass('hidden'); + commentElt.html(""); + + $j.get(baseUrl + "/issue/edit_comment_form/" + commentKey, function (html) { + commentElt.html(html); + }); + return false; +} + +function doEditIssueComment(elt) { + var formElt = $j(elt).closest('form'); + var issueElt = formElt.closest('[data-issue-key]'); + var issueKey = issueElt.attr('data-issue-key'); + $j.ajax({ + type: "POST", + url: baseUrl + "/issue/edit_comment", + data: formElt.serialize(), + success: function (htmlResponse) { + var replaced = $j(htmlResponse); + issueElt.replaceWith(replaced); + notifyIssueChange(issueKey); + }, + error: function (jqXHR, textStatus) { + closeIssueForm(elt); + var commentElt = formElt.closest('[data-comment-key]'); + commentElt.replaceWith(jqXHR.responseText); + } + }); + return false; +} + +function refreshIssue(elt) { + var issueElt = $j(elt).closest('[data-issue-key]'); + var issueKey = issueElt.attr('data-issue-key'); + $j.get(baseUrl + "/issue/show/" + issueKey + "?only_detail=true", function (html) { + var replaced = $j(html); + issueElt.replaceWith(replaced); + }); + return false; +} + +/* Open form for creating a manual issue */ +function openCIF(elt, componentId, line) { + // TODO check if form is already displayed (by using form id) + $j.get(baseUrl + "/issue/create_form?component=" + componentId + "&line=" + line, function (html) { + $j(elt).closest('tr').find('td.line').append($j(html)); + }); + return false; +} + +/* Close the form used for creating a manual issue */ +function closeCreateIssueForm(elt) { + $j(elt).closest('.code-issue-create-form').remove(); + return false; +} + +/* Create a manual issue */ +function submitCreateIssueForm(elt) { + var formElt = $j(elt).closest('form'); + var loadingElt = formElt.find('.loading'); + + loadingElt.removeClass('hidden'); + $j.ajax({ + type: "POST", + url: baseUrl + '/issue/create', + data: formElt.serialize()} + ).success(function (html) { + var replaced = $j(html); + formElt.replaceWith(replaced); + } + ).error(function (jqXHR, textStatus, errorThrown) { + var errorsElt = formElt.find('.code-issue-errors'); + errorsElt.html(jqXHR.responseText); + errorsElt.removeClass('hidden'); + } + ).always(function () { + loadingElt.addClass('hidden'); + }); + return false; +} + +function toggleIssueCollapsed(elt) { + var issueElt = $j(elt).closest('[data-issue-rule]'); + issueElt.toggleClass('code-issue-collapsed'); + + if (!issueElt.hasClass('code-issue-collapsed')) { + + // Load rule desc + // Display loading images and hide existing content + var ruleLoading = issueElt.find('.rule-loading'); + ruleLoading.removeClass('hidden'); + var ruleElt = issueElt.find('.issue-rule'); + ruleElt.addClass('hidden'); + var ruleKey = issueElt.attr('data-issue-rule'); + $j.get(baseUrl + "/issue/rule/" + ruleKey, function (html) { + ruleElt.html(html); + // re-enable the links opening modal popups + ruleElt.find('.open-modal').modal(); + }).always(function () { + ruleLoading.addClass('hidden'); + ruleElt.removeClass('hidden'); + }); + + // Load changelog + // Display loading images and hide existing content + var cangelogLoading = issueElt.find('.changelog-loading'); + cangelogLoading.removeClass('hidden'); + var changelogElt = issueElt.find('.issue-changelog'); + changelogElt.addClass('hidden'); + var issueKey = issueElt.attr('data-issue-key'); + $j.get(baseUrl + "/issue/changelog/" + issueKey, function (html) { + changelogElt.html(html); + }).always(function () { + cangelogLoading.addClass('hidden'); + changelogElt.removeClass('hidden'); + }); + } + return false; +} + +function openIssuePopup(elt) { + var issueElt = $j(elt).closest('[data-issue-key]'); + var issueKey = issueElt.attr('data-issue-key'); + openPopup(baseUrl + "/issue/show/" + issueKey + "?layout=false", 'issue'); + return false; +} diff --git a/sonar-server/src/main/js/issues/app.js b/sonar-server/src/main/js/issues/app.js new file mode 100644 index 00000000000..4a291cc45f7 --- /dev/null +++ b/sonar-server/src/main/js/issues/app.js @@ -0,0 +1,402 @@ +requirejs.config({ + baseUrl: baseUrl + '/js', + + paths: { + 'backbone': 'third-party/backbone', + 'backbone.marionette': 'third-party/backbone.marionette', + 'handlebars': 'third-party/handlebars', + 'moment': 'third-party/moment' + }, + + shim: { + 'backbone.marionette': { + deps: ['backbone'], + exports: 'Marionette' + }, + 'backbone': { + exports: 'Backbone' + }, + 'handlebars': { + exports: 'Handlebars' + }, + 'moment': { + exports: 'moment' + } + } + +}); + +requirejs( + [ + 'backbone', 'backbone.marionette', 'handlebars', 'moment', + 'issues/extra', + 'navigator/filters/filter-bar', + 'navigator/filters/base-filters', + 'navigator/filters/checkbox-filters', + 'navigator/filters/choice-filters', + 'navigator/filters/ajax-select-filters', + 'navigator/filters/favorite-filters', + 'navigator/filters/range-filters', + 'navigator/filters/context-filters', + 'navigator/filters/read-only-filters', + 'navigator/filters/action-plan-filters', + 'navigator/filters/rule-filters', + + 'common/handlebars-extensions' + ], + function (Backbone, Marionette, Handlebars, moment, Extra, FilterBar, BaseFilters, CheckboxFilterView, + ChoiceFilters, AjaxSelectFilters, FavoriteFilters, RangeFilters, ContextFilterView, + ReadOnlyFilterView, ActionPlanFilterView, RuleFilterView) { + Handlebars.registerPartial('detailInnerTemplate', jQuery('#issue-detail-inner-template').html()); + + var NavigatorApp = new Marionette.Application(); + + + NavigatorApp.addRegions({ + headerRegion: '.navigator-header', + filtersRegion: '.navigator-filters', + resultsRegion: '.navigator-results', + actionsRegion: '.navigator-actions', + detailsRegion: '.navigator-details' + }); + + + // Adjust details region height + jQuery('.navigator-details').css('bottom', jQuery('#footer').outerHeight()); + + + NavigatorApp.addInitializer(function () { + jQuery('html').addClass('navigator-page issues-page'); + + this.appState = new Extra.AppState(); + window.SS.appState = this.appState; + + this.state = new Backbone.Model({ + query: '' + }); + + this.issues = new Extra.Issues(); + this.issues.sorting = { + sort: 'UPDATE_DATE', + asc: false + }; + this.issuesPage = 1; + + this.filters = new BaseFilters.Filters(); + + this.favoriteFilter = new Extra.FavoriteFilter(); + this.issuesHeaderView = new Extra.IssuesHeaderView({ + app: this, + model: this.favoriteFilter + }); + this.headerRegion.show(this.issuesHeaderView); + + this.issuesView = new Extra.IssuesView({ + app: this, + collection: this.issues + }); + this.resultsRegion.show(this.issuesView); + + this.issuesActionsView = new Extra.IssuesActionsView({ + app: this, + collection: this.issues + }); + this.actionsRegion.show(this.issuesActionsView); + }); + + + NavigatorApp.addInitializer(function () { + var projectFilter = new BaseFilters.Filter({ + name: window.SS.phrases.project, + property: 'componentRoots', + type: AjaxSelectFilters.ProjectFilterView, + enabled: true, + optional: false + }); + this.filters.add(projectFilter); + + this.filters.add([ + new BaseFilters.Filter({ + name: window.SS.phrases.severity, + property: 'severities', + type: ChoiceFilters.ChoiceFilterView, + enabled: true, + optional: false, + choices: { + 'BLOCKER': window.SS.phrases.severities.BLOCKER, + 'CRITICAL': window.SS.phrases.severities.CRITICAL, + 'MAJOR': window.SS.phrases.severities.MAJOR, + 'MINOR': window.SS.phrases.severities.MINOR, + 'INFO': window.SS.phrases.severities.INFO + }, + choiceIcons: { + 'BLOCKER': 'severity-blocker', + 'CRITICAL': 'severity-critical', + 'MAJOR': 'severity-major', + 'MINOR': 'severity-minor', + 'INFO': 'severity-info' + } + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.status, + property: 'statuses', + type: ChoiceFilters.ChoiceFilterView, + enabled: true, + optional: false, + choices: { + 'OPEN': window.SS.phrases.statuses.OPEN, + 'CONFIRMED': window.SS.phrases.statuses.CONFIRMED, + 'REOPENED': window.SS.phrases.statuses.REOPENED, + 'RESOLVED': window.SS.phrases.statuses.RESOLVED, + 'CLOSED': window.SS.phrases.statuses.CLOSED + }, + choiceIcons: { + 'OPEN': 'status-open', + 'CONFIRMED': 'status-confirmed', + 'REOPENED': 'status-reopened', + 'RESOLVED': 'status-resolved', + 'CLOSED': 'status-closed' + } + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.assignee, + property: 'assignees', + type: AjaxSelectFilters.AssigneeFilterView, + enabled: true, + optional: false, + choices: { + '!assigned': window.SS.phrases.unassigned + } + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.resolution, + property: 'resolutions', + type: ChoiceFilters.ChoiceFilterView, + enabled: true, + optional: false, + choices: { + '!resolved': window.SS.phrases.resolutions.UNRESOLVED, + 'FALSE-POSITIVE': window.SS.phrases.resolutions['FALSE-POSITIVE'], + 'FIXED': window.SS.phrases.resolutions.FIXED, + 'REMOVED': window.SS.phrases.resolutions.REMOVED + } + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.actionPlan, + property: 'actionPlans', + type: ActionPlanFilterView, + enabled: false, + optional: true, + projectFilter: projectFilter, + choices: { + '!planned': window.SS.phrases.unplanned + } + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.created, + propertyFrom: 'createdAfter', + propertyTo: 'createdBefore', + type: RangeFilters.DateRangeFilterView, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.createdAt, + property: 'createdAt', + type: ReadOnlyFilterView, + enabled: false, + optional: true, + format: function(value) { return moment(value).format('YYYY-MM-DD HH:mm'); } + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.reporter, + property: 'reporters', + type: AjaxSelectFilters.ReporterFilterView, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.rule, + property: 'rules', + type: RuleFilterView, + enabled: false, + optional: true + }) + + ]); + + + this.filterBarView = new Extra.IssuesFilterBarView({ + app: this, + collection: this.filters, + extra: { + sort: '', + asc: false + } + }); + + this.filtersRegion.show(this.filterBarView); + }); + + + NavigatorApp.addInitializer(function () { + var app = this; + + jQuery.when(this.appState.fetch()).done(function () { + + if (app.appState.get('favorites')) { + app.filters.unshift( + new BaseFilters.Filter({ + type: Extra.IssuesFavoriteFilterView, + enabled: true, + optional: false, + choices: app.appState.get('favorites'), + manageUrl: '/issues/manage' + }) + ); + } + + app.router = new Extra.IssuesRouter({ + app: app + }); + Backbone.history.start(); + + app.favoriteFilter.on('change:query', function (model, query) { + app.router.navigate(query, { trigger: true, replace: true }); + }); + }); + }); + + + NavigatorApp.addInitializer(function () { + var app = this; + + window.onBulkIssues = function () { + app.fetchFirstPage(); + jQuery('.ui-dialog, .ui-widget-overlay').remove(); + }; + + window.onSaveAs = window.onCopy = window.onEdit = function (id) { + jQuery('#modal').dialog('close'); + app.appState.fetch(); + + var filter = new Extra.FavoriteFilter({ id: id }); + filter.fetch({ + success: function () { + app.state.set('search', false); + app.favoriteFilter.set(filter.toJSON()); + app.fetchFirstPage(); + } + }); + }; + }); + + + NavigatorApp.getQuery = function (withoutId) { + var query = this.filterBarView.getQuery(); + if (!withoutId && this.favoriteFilter.id) { + query['id'] = this.favoriteFilter.id; + } + return query; + }; + + + NavigatorApp.storeQuery = function (query, sorting) { + if (sorting) { + _.extend(query, { + sort: sorting.sort, + asc: '' + sorting.asc + }); + } + + var queryString = _.map(query,function (v, k) { + return [k, encodeURIComponent(v)].join('='); + }).join('|'); + this.router.navigate(queryString, { replace: true }); + }; + + + NavigatorApp.restoreSorting = function (query) { + var sort = _.findWhere(query, { key: 'sort' }), + asc = _.findWhere(query, { key: 'asc' }); + + if (sort && asc) { + this.issues.sorting = { + sort: sort.value, + sortText: jQuery('[data-sort=' + sort.value + ']:first').text(), + asc: asc.value === 'true' + } + } + }; + + + NavigatorApp.fetchIssues = function (firstPage) { + var query = this.getQuery(), + fetchQuery = _.extend({ + pageIndex: this.issuesPage + }, query); + + if (this.issues.sorting) { + _.extend(fetchQuery, { + sort: this.issues.sorting.sort, + asc: this.issues.sorting.asc + }); + } + + _.extend(fetchQuery, { + hideRules: true + }); + + if (this.favoriteFilter.id) { + query['id'] = this.favoriteFilter.id; + fetchQuery['id'] = this.favoriteFilter.id; + } + + this.storeQuery(query, this.issues.sorting); + + var that = this; + this.issuesView.$el.addClass('navigator-fetching'); + if (firstPage) { + this.issues.fetch({ + data: fetchQuery, + success: function () { + that.issuesView.$el.removeClass('navigator-fetching'); + } + }); + this.detailsRegion.reset(); + } else { + this.issues.fetch({ + data: fetchQuery, + remove: false, + success: function () { + that.issuesView.$el.removeClass('navigator-fetching'); + } + }); + } + }; + + + NavigatorApp.fetchFirstPage = function () { + this.issuesPage = 1; + this.fetchIssues(true); + }; + + + NavigatorApp.fetchNextPage = function () { + if (this.issuesPage < this.issues.paging.pages) { + this.issuesPage++; + this.fetchIssues(false); + } + }; + + NavigatorApp.start(); + + }); diff --git a/sonar-server/src/main/js/issues/extra.js b/sonar-server/src/main/js/issues/extra.js new file mode 100644 index 00000000000..66fa821b014 --- /dev/null +++ b/sonar-server/src/main/js/issues/extra.js @@ -0,0 +1,1242 @@ +define( + [ + 'backbone', 'backbone.marionette', + '../navigator/filters/filter-bar', + 'navigator/filters/base-filters', + 'navigator/filters/favorite-filters', + 'navigator/filters/read-only-filters' + ], + function (Backbone, Marionette, FilterBarView, BaseFilters, FavoriteFiltersModule, ReadOnlyFilterView) { + + var AppState = Backbone.Model.extend({ + + defaults: { + canManageFilters: false, + canBulkChange: false + }, + + + url: function () { + return baseUrl + '/api/issue_filters/app'; + } + + }); + + + var Issue = Backbone.Model.extend({ + + url: function () { + return baseUrl + '/api/issues/show?key=' + this.get('key'); + }, + + + parse: function (r) { + return r.issue ? r.issue : r; + } + + }); + + + var Issues = Backbone.Collection.extend({ + model: Issue, + + + url: function () { + return baseUrl + '/api/issues/search'; + }, + + + parse: function (r) { + + function find(source, key, keyField) { + var searchDict = {}; + searchDict[keyField || 'key'] = key; + return _.findWhere(source, searchDict) || key; + } + + this.paging = r.paging; + this.maxResultsReached = r.maxResultsReached; + + return r.issues.map(function (issue) { + var component = find(r.components, issue.component), + project = find(r.projects, issue.project), + rule = find(r.rules, issue.rule); + + if (component) { + _.extend(issue, { + componentLongName: component.longName, + componentQualifier: component.qualifier + }); + } + + if (project) { + _.extend(issue, { + projectLongName: project.longName + }); + } + + if (rule) { + _.extend(issue, { + ruleName: rule.name + }); + } + + return issue; + }); + + } + }); + + + var FavoriteFilter = Backbone.Model.extend({ + + url: function () { + return baseUrl + '/api/issue_filters/show/' + this.get('id'); + }, + + + parse: function (r) { + return r.filter ? r.filter : r; + } + }); + + + var FavoriteFilters = Backbone.Collection.extend({ + model: FavoriteFilter, + + + url: function () { + return baseUrl + '/api/issue_filters/favorites'; + }, + + + parse: function (r) { + return r.favoriteFilters; + } + }); + + + var Rule = Backbone.Model.extend({ + + url: function () { + return baseUrl + '/api/rules/show/?key=' + this.get('key'); + }, + + + parse: function (r) { + return r.rule ? r.rule : r; + } + }); + + + var ActionPlans = Backbone.Collection.extend({ + + url: function () { + return baseUrl + '/api/action_plans/search'; + }, + + + parse: function (r) { + return r.actionPlans; + } + + }); + + + var IssueView = Marionette.ItemView.extend({ + template: Handlebars.compile(jQuery('#issue-template').html() || ''), + tagName: 'li', + + + ui: { + component: '.component' + }, + + + events: { + 'click': 'showDetails' + }, + + + modelEvents: { + 'change': 'render' + }, + + + showDetails: function () { + this.$el.parent().children().removeClass('active'); + this.$el.addClass('active'); + + var that = this, + app = this.options.app, + detailView = new IssueDetailView({ + model: this.model + }), + showCallback = function () { + jQuery('.navigator-details').removeClass('navigator-fetching'); + app.detailsRegion.show(detailView); + }; + + jQuery('.navigator-details').empty().addClass('navigator-fetching'); + jQuery.when(detailView.model.fetch()).done(function () { + if (that.model.get('status') !== 'CLOSED') { + that.fetchSource(detailView, showCallback); + } else { + showCallback(); + } + + }); + }, + + + fetchSource: function (view, callback) { + var line = this.model.get('line') || 0, + from = line >= 10 ? line - 10 : 0, + to = line + 30; + + return jQuery + .ajax({ + type: 'GET', + url: baseUrl + '/api/sources/show', + data: { + key: this.model.get('component'), + from: from, + to: to, + format: 'json' + } + }) + .done(function (r) { + if (_.isObject(r) && r.source) { + view.source = r.source; + } + if (_.isObject(r) && r.scm) { + view.scm = r.scm; + } + }) + .always(callback); + }, + + + serializeData: function () { + var projectFilter = this.options.app.filters.findWhere({ property: 'componentRoots' }), + singleProject = _.isArray(projectFilter.get('value')) && projectFilter.get('value').length === 1; + + return _.extend({ + singleProject: singleProject + }, this.model.toJSON()); + } + }); + + + var NoIssuesView = Marionette.ItemView.extend({ + tagName: 'li', + className: 'navigator-results-no-results', + template: Handlebars.compile(jQuery('#no-issues-template').html() || '') + }); + + + var IssuesView = Marionette.CollectionView.extend({ + tagName: 'ol', + className: 'navigator-results-list', + itemView: IssueView, + emptyView: NoIssuesView, + + + itemViewOptions: function () { + return { + issuesView: this, + app: this.options.app + }; + }, + + + onRender: function () { + var that = this, + $scrollEl = jQuery('.navigator-results'), + scrollEl = $scrollEl.get(0), + onScroll = function () { + if (scrollEl.offsetHeight + scrollEl.scrollTop >= scrollEl.scrollHeight) { + that.options.app.fetchNextPage(); + } + }, + throttledScroll = _.throttle(onScroll, 300); + $scrollEl.off('scroll').on('scroll', throttledScroll); + }, + + + onAfterItemAdded: function () { + var showLimitNotes = this.collection.maxResultsReached != null && this.collection.maxResultsReached; + jQuery('.navigator').toggleClass('navigator-with-notes', showLimitNotes); + jQuery('.navigator-notes').toggle(showLimitNotes); + }, + + + close: function () { + var scrollEl = jQuery('.navigator-results'); + scrollEl.off('scroll'); + Marionette.CollectionView.prototype.close.call(this); + } + + }); + + + var IssuesActionsView = Marionette.ItemView.extend({ + template: Handlebars.compile(jQuery('#issues-actions-template').html() || ''), + + + collectionEvents: { + 'sync': 'render' + }, + + + events: { + 'click .navigator-actions-order': 'toggleOrderChoices', + 'click .navigator-actions-order-choices': 'sort', + 'click .navigator-actions-bulk': 'bulkChange' + }, + + + ui: { + orderChoices: '.navigator-actions-order-choices' + }, + + + onRender: function () { + if (!this.collection.sorting.sortText) { + this.collection.sorting.sortText = this.$('[data-sort=' + this.collection.sorting.sort + ']:first').text(); + this.$('.navigator-actions-ordered-by').text(this.collection.sorting.sortText); + } + }, + + + toggleOrderChoices: function (e) { + e.stopPropagation(); + this.ui.orderChoices.toggleClass('open'); + if (this.ui.orderChoices.is('.open')) { + var that = this; + jQuery('body').on('click.issues_actions', function () { + that.ui.orderChoices.removeClass('open'); + }); + } + }, + + + sort: function (e) { + e.stopPropagation(); + this.ui.orderChoices.removeClass('open'); + jQuery('body').off('click.issues_actions'); + var el = jQuery(e.target), + sort = el.data('sort'), + asc = el.data('asc'); + + if (sort != null && asc != null) { + this.collection.sorting = { + sort: sort, + sortText: el.text(), + asc: asc + }; + this.options.app.fetchFirstPage(); + } + }, + + + bulkChange: function(e) { + e.preventDefault(); + openModalWindow(jQuery(e.currentTarget).prop('href'), {}); + }, + + + serializeData: function () { + var data = Marionette.ItemView.prototype.serializeData.apply(this, arguments); + return _.extend(data || {}, { + paging: this.collection.paging, + sorting: this.collection.sorting, + maxResultsReached: this.collection.maxResultsReached, + appState: window.SS.appState.toJSON(), + query: (Backbone.history.fragment || '').replace(/\|/g, '&') + }); + } + }); + + + var IssuesFilterBarView = FilterBarView.extend({ + + collectionEvents: { + 'change:enabled': 'changeEnabled' + }, + + + events: { + 'click .navigator-filter-submit': 'search' + }, + + + getQuery: function () { + var query = {}; + this.collection.each(function (filter) { + _.extend(query, filter.view.formatValue()); + }); + return query; + }, + + + onAfterItemAdded: function (itemView) { + if (itemView.model.get('type') === FavoriteFiltersModule.FavoriteFilterView || + itemView.model.get('type') === IssuesFavoriteFilterView) { + jQuery('.navigator-header').addClass('navigator-header-favorite'); + } + }, + + + addMoreCriteriaFilter: function() { + var readOnlyFilters = this.collection.where({ type: ReadOnlyFilterView }), + disabledFilters = _.difference(this.collection.where({ enabled: false }), readOnlyFilters); + this.moreCriteriaFilter = new BaseFilters.Filter({ + type: require('navigator/filters/more-criteria-filters').MoreCriteriaFilterView, + enabled: true, + optional: false, + filters: disabledFilters + }); + this.collection.add(this.moreCriteriaFilter); + }, + + + changeEnabled: function () { + var disabledFilters = _.reject(this.collection.where({ enabled: false }), function (filter) { + return filter.get('type') === require('navigator/filters/more-criteria-filters').MoreCriteriaFilterView || + filter.get('type') === ReadOnlyFilterView; + }); + + if (disabledFilters.length === 0) { + this.moreCriteriaFilter.set({ enabled: false }, { silent: true }); + } else { + this.moreCriteriaFilter.set({ enabled: true }, { silent: true }); + } + this.moreCriteriaFilter.set({ filters: disabledFilters }, { silent: true }); + this.moreCriteriaFilter.trigger('change:filters'); + }, + + + search: function () { + this.options.app.state.set({ + query: this.options.app.getQuery(), + search: true + }); + this.options.app.fetchFirstPage(); + }, + + + fetchNextPage: function () { + this.options.app.fetchNextPage(); + } + + }); + + + var IssuesHeaderView = Marionette.ItemView.extend({ + template: Handlebars.compile(jQuery('#issues-header-template').html() || ''), + + + modelEvents: { + 'change': 'render' + }, + + + events: { + 'click #issues-new-search': 'newSearch', + 'click #issues-filter-save-as': 'saveAs', + 'click #issues-filter-save': 'save', + 'click #issues-filter-copy': 'copy', + 'click #issues-filter-edit': 'edit' + }, + + + initialize: function (options) { + Marionette.ItemView.prototype.initialize.apply(this, arguments); + this.listenTo(options.app.state, 'change', this.render); + }, + + + newSearch: function () { + this.model.clear(); + this.options.app.router.navigate('resolved=false', { trigger: true, replace: true }); + }, + + + saveAs: function () { + var url = baseUrl + '/issues/save_as_form?' + (Backbone.history.fragment || '').replace(/\|/g, '&'); + openModalWindow(url, {}); + }, + + + save: function () { + var that = this; + url = baseUrl + '/issues/save/' + this.model.id + '?' + (Backbone.history.fragment || '').replace(/\|/g, '&'); + jQuery.ajax({ + type: 'POST', + url: url + }).done(function () { + that.options.app.state.set('search', false); + }); + }, + + + copy: function () { + var url = baseUrl + '/issues/copy_form/' + this.model.id; + openModalWindow(url, {}); + }, + + + edit: function () { + var url = baseUrl + '/issues/edit_form/' + this.model.id; + openModalWindow(url, {}); + }, + + + serializeData: function () { + return _.extend({ + canSave: this.model.id && this.options.app.state.get('search'), + appState: window.SS.appState.toJSON(), + currentUser: window.SS.currentUser + }, this.model.toJSON()); + } + + }); + + + var IssueDetailCommentFormView = Marionette.ItemView.extend({ + template: Handlebars.compile(jQuery('#issue-detail-comment-form-template').html() || ''), + + + ui: { + textarea: '#issue-comment-text', + cancelButton: '#issue-comment-cancel', + submitButton: '#issue-comment-submit' + }, + + + events: { + 'keyup #issue-comment-text': 'toggleSubmit', + 'click #issue-comment-cancel': 'cancel', + 'click #issue-comment-submit': 'submit' + }, + + + onDomRefresh: function () { + this.ui.textarea.focus(); + }, + + + toggleSubmit: function () { + this.ui.submitButton.prop('disabled', this.ui.textarea.val().length === 0); + }, + + + cancel: function () { + this.options.detailView.updateAfterAction(false); + }, + + + submit: function () { + var that = this, + text = this.ui.textarea.val(), + update = this.model && this.model.has('key'), + url = baseUrl + '/api/issues/' + (update ? 'edit_comment' : 'add_comment'), + data = { text: text }; + + if (update) { + data.key = this.model.get('key'); + } else { + data.issue = this.options.issue.get('key'); + } + + this.options.detailView.showActionSpinner(); + + jQuery.ajax({ + type: 'POST', + url: url, + data: data + }) + .done(function () { + that.options.detailView.updateAfterAction(true); + }) + .fail(function (r) { + alert(r.responseJSON.errors ? _.pluck(r.responseJSON.errors, 'msg').join(' ') : r); + that.options.detailView.hideActionSpinner(); + }); + } + }); + + + var IssueDetailSetSeverityFormView = Marionette.ItemView.extend({ + template: Handlebars.compile(jQuery('#issue-detail-set-severity-form-template').html() || ''), + + + ui: { + select: '#issue-set-severity-select' + }, + + + events: { + 'click #issue-set-severity-cancel': 'cancel', + 'click #issue-set-severity-submit': 'submit' + }, + + + onRender: function () { + var format = function(state) { + if (!state.id) return state.text; // optgroup + return ' ' + state.text; + } + + this.ui.select.select2({ + minimumResultsForSearch: 100, + formatResult: format, + formatSelection: format, + escapeMarkup: function(m) { return m; } + }); + }, + + + cancel: function () { + this.options.detailView.updateAfterAction(false); + }, + + + submit: function () { + var that = this; + + this.options.detailView.showActionSpinner(); + + jQuery.ajax({ + type: 'POST', + url: baseUrl + '/api/issues/set_severity', + data: { + issue: this.options.issue.get('key'), + severity: this.ui.select.val() + } + }) + .done(function () { + that.options.detailView.updateAfterAction(true); + }) + .fail(function (r) { + alert(r.responseJSON.errors ? _.pluck(r.responseJSON.errors, 'msg').join(' ') : r); + that.options.detailView.hideActionSpinner(); + }); + } + }); + + + var IssueDetailAssignFormView = Marionette.ItemView.extend({ + template: Handlebars.compile(jQuery('#issue-detail-assign-form-template').html() || ''), + + + ui: { + select: '#issue-assignee-select' + }, + + + events: { + 'click #issue-assign-cancel': 'cancel', + 'click #issue-assign-submit': 'submit' + }, + + + onRender: function () { + var currentUser = window.SS.currentUser, + assignee = this.options.issue.get('assignee'), + additionalChoices = []; + + if (!assignee || currentUser !== assignee) { + additionalChoices.push({ + id: currentUser, + text: window.SS.phrases.assignedToMe + }); + } + + if (!!assignee) { + additionalChoices.push({ + id: '', + text: window.SS.phrases.unassigned + }); + } + + var select2Options = { + allowClear: false, + width: '250px', + formatNoMatches: function () { + return window.SS.phrases.select2.noMatches; + }, + formatSearching: function () { + return window.SS.phrases.select2.searching; + }, + formatInputTooShort: function () { + return window.SS.phrases.select2.tooShort; + } + }; + + if (additionalChoices.length > 0) { + select2Options.minimumInputLength = 0; + select2Options.query = function (query) { + if (query.term.length == 0) { + query.callback({ results: additionalChoices }); + } else if (query.term.length >= 2) { + jQuery.ajax({ + url: baseUrl + '/api/users/search?f=s2', + data: { s: query.term }, + dataType: 'jsonp' + }).done(function (data) { + query.callback(data); + }); + } + } + } else { + select2Options.minimumInputLength = 2; + select2Options.ajax = { + quietMillis: 300, + url: baseUrl + '/api/users/search?f=s2', + data: function (term, page) { + return {s: term, p: page} + }, + results: function (data) { + return { more: data.more, results: data.results } + } + }; + } + + this.ui.select.select2(select2Options).select2('open'); + }, + + + cancel: function () { + this.options.detailView.updateAfterAction(false); + }, + + + submit: function () { + var that = this; + + this.options.detailView.showActionSpinner(); + + jQuery.ajax({ + type: 'POST', + url: baseUrl + '/api/issues/assign', + data: { + issue: this.options.issue.get('key'), + assignee: this.ui.select.val() + } + }) + .done(function () { + that.options.detailView.updateAfterAction(true); + }) + .fail(function (r) { + alert(r.responseJSON.errors ? _.pluck(r.responseJSON.errors, 'msg').join(' ') : r); + that.options.detailView.hideActionSpinner(); + }); + } + }); + + + var IssueDetailPlanFormView = Marionette.ItemView.extend({ + template: Handlebars.compile(jQuery('#issue-detail-plan-form-template').html() || ''), + + + collectionEvents: { + 'reset': 'render' + }, + + + ui: { + select: '#issue-detail-plan-select' + }, + + + events: { + 'click #issue-plan-cancel': 'cancel', + 'click #issue-plan-submit': 'submit' + }, + + + onRender: function () { + this.ui.select.select2({ + width: '250px', + minimumResultsForSearch: 100 + }); + + this.$('.error a') + .prop('href', baseUrl + '/action_plans/index/' + this.options.issue.get('project')); + }, + + + cancel: function () { + this.options.detailView.updateAfterAction(false); + }, + + + submit: function () { + var that = this, + plan = this.ui.select.val(); + + this.options.detailView.showActionSpinner(); + + jQuery.ajax({ + type: 'POST', + url: baseUrl + '/api/issues/plan', + data: { + issue: this.options.issue.get('key'), + plan: plan === '#unplan' ? '' : plan + } + }) + .done(function () { + that.options.detailView.updateAfterAction(true); + }) + .fail(function (r) { + alert(r.responseJSON.errors ? _.pluck(r.responseJSON.errors, 'msg').join(' ') : r); + that.options.detailView.hideActionSpinner(); + }); + }, + + + serializeData: function () { + return { + items: this.collection.toJSON(), + issue: this.options.issue.toJSON() + } + } + }); + + + var IssueDetailRuleView = Marionette.ItemView.extend({ + template: Handlebars.compile(jQuery('#issue-detail-rule-template').html() || ''), + className: 'rule-desc', + modelEvents: { 'change': 'render' }, + + + serializeData: function () { + return _.extend({ + characteristic: this.options.issue.get('characteristic'), + subCharacteristic: this.options.issue.get('subCharacteristic') + }, this.model.toJSON()); + } + }); + + + var IssueDetailView = Marionette.Layout.extend({ + template: Handlebars.compile(jQuery('#issue-detail-template').html() || ''), + + + regions: { + formRegion: '.code-issue-form', + ruleRegion: '#tab-issue-rule' + }, + + + events: { + 'click .code-issue-toggle': 'toggleCollapsed', + + 'click [href=#tab-issue-rule]': 'fetchRule', + + 'click #issue-comment': 'comment', + 'click .issue-comment-edit': 'editComment', + 'click .issue-comment-delete': 'deleteComment', + 'click .issue-transition': 'transition', + 'click #issue-set-severity': 'setSeverity', + 'click #issue-assign': 'assign', + 'click #issue-assign-to-me': 'assignToMe', + 'click #issue-plan': 'plan', + 'click .issue-action': 'action' + }, + + + modelEvents: { + 'change': 'render' + }, + + + onRender: function () { + this.$('.code-issue-details').tabs(); + this.$('.code-issue-form').hide(); + this.rule = new Rule({ key: this.model.get('rule') }); + this.ruleRegion.show(new IssueDetailRuleView({ + model: this.rule, + issue: this.model + })); + this.initReferenceLinks(); + }, + + + initReferenceLinks: function () { + var sourcesId = 'sources_' + this.model.get('key'); + this.$('#' + sourcesId).on('click', 'span.sym', { id: sourcesId }, highlight_usages); + }, + + + onDomRefresh: function () { + var sourceTitleHeight = this.$('.source_title').outerHeight(); + jQuery('.navigator-details').css('padding-top', (sourceTitleHeight + 10) + 'px'); + }, + + + onClose: function () { + if (this.ruleRegion) { + this.ruleRegion.reset(); + } + }, + + + resetIssue: function (options) { + var key = this.model.get('key'); + this.model.clear({ silent: true }); + this.model.set({ key: key }, { silent: true }); + return this.model.fetch(options); + }, + + + toggleCollapsed: function () { + this.$('.code-issue').toggleClass('code-issue-collapsed'); + this.fetchRule(); + }, + + + fetchRule: function () { + var that = this; + if (!this.rule.has('name')) { + this.$('#tab-issue-rule').addClass('navigator-fetching'); + this.rule.fetch({ + success: function () { + that.$('#tab-issue-rule').removeClass('navigator-fetching'); + } + }); + } + }, + + + showActionView: function (view) { + this.$('.code-issue-actions').hide(); + this.$('.code-issue-form').show(); + this.formRegion.show(view); + }, + + + showActionSpinner: function () { + this.$('.code-issue-actions').addClass('navigator-fetching'); + }, + + + hideActionSpinner: function () { + this.$('.code-issue-actions').removeClass('navigator-fetching'); + }, + + + updateAfterAction: function (fetch) { + var that = this; + + that.formRegion.reset(); + that.$('.code-issue-actions').show(); + that.$('.code-issue-form').hide(); + that.$('[data-comment-key]').show(); + + if (fetch) { + jQuery.when(this.resetIssue()).done(function () { + that.hideActionSpinner(); + }); + } + }, + + + comment: function () { + var commentFormView = new IssueDetailCommentFormView({ + issue: this.model, + detailView: this + }); + this.showActionView(commentFormView); + }, + + + editComment: function (e) { + var commentEl = jQuery(e.target).closest('[data-comment-key]'), + commentKey = commentEl.data('comment-key'), + comment = _.findWhere(this.model.get('comments'), { key: commentKey }); + + commentEl.hide(); + + var commentFormView = new IssueDetailCommentFormView({ + model: new Backbone.Model(comment), + issue: this.model, + detailView: this + }); + this.showActionView(commentFormView); + }, + + + deleteComment: function (e) { + var that = this, + commentKey = jQuery(e.target).closest('[data-comment-key]').data('comment-key'), + confirmMsg = jQuery(e.target).data('confirm-msg'); + + if (confirm(confirmMsg)) { + this.showActionSpinner(); + + jQuery.ajax({ + type: "POST", + url: baseUrl + "/issue/delete_comment?id=" + commentKey + }) + .done(function () { + that.updateAfterAction(true); + }) + .fail(function (r) { + alert(r.responseJSON.errors ? _.pluck(r.responseJSON.errors, 'msg').join(' ') : r); + that.hideActionSpinner(); + }); + } + }, + + + transition: function (e) { + var that = this; + + this.showActionSpinner(); + + jQuery.ajax({ + type: 'POST', + url: baseUrl + '/api/issues/do_transition', + data: { + issue: this.model.get('key'), + transition: jQuery(e.target).data('transition') + } + }) + .done(function () { + that.resetIssue(); + }) + .fail(function (r) { + alert(r.responseJSON.errors ? _.pluck(r.responseJSON.errors, 'msg').join(' ') : r); + that.hideActionSpinner(); + }); + }, + + + setSeverity: function () { + var setSeverityFormView = new IssueDetailSetSeverityFormView({ + issue: this.model, + detailView: this + }); + this.showActionView(setSeverityFormView); + }, + + + assign: function () { + var assignFormView = new IssueDetailAssignFormView({ + issue: this.model, + detailView: this + }); + this.showActionView(assignFormView); + }, + + + assignToMe: function () { + var that = this; + + this.showActionSpinner(); + + jQuery.ajax({ + type: 'POST', + url: baseUrl + '/api/issues/assign', + data: { + issue: this.model.get('key'), + assignee: window.SS.currentUser + } + }) + .done(function () { + that.resetIssue(); + }) + .fail(function (r) { + alert(r.responseJSON.errors ? _.pluck(r.responseJSON.errors, 'msg').join(' ') : r); + that.hideActionSpinner(); + }); + }, + + + plan: function () { + var that = this, + actionPlans = new ActionPlans(), + planFormView = new IssueDetailPlanFormView({ + collection: actionPlans, + issue: this.model, + detailView: this + }); + + this.showActionSpinner(); + + actionPlans.fetch({ + reset: true, + data: { project: this.model.get('project') }, + success: function () { + that.hideActionSpinner(); + that.showActionView(planFormView); + } + }); + }, + + action: function (e) { + var that = this, + actionKey = jQuery(e.target).data('action'); + + this.showActionSpinner(); + + jQuery.ajax({ + type: 'POST', + url: baseUrl + '/api/issues/do_action', + data: { + issue: this.model.get('key'), + actionKey: actionKey + } + }) + .done(function () { + that.resetIssue(); + }) + .fail(function (r) { + alert(r.responseJSON.errors ? _.pluck(r.responseJSON.errors, 'msg').join(' ') : r); + that.hideActionSpinner(); + }); + }, + + + serializeData: function () { + return _.extend({ + source: this.source, + scm: this.scm + }, this.model.toJSON()); + } + + }); + + + var IssuesDetailsFavoriteFilterView = FavoriteFiltersModule.DetailsFavoriteFilterView.extend({ + template: Handlebars.compile(jQuery('#issues-details-favorite-filter-template').html() || ''), + + + applyFavorite: function (e) { + var id = $j(e.target).data('id'), + filter = new FavoriteFilter({ id: id }), + app = this.options.filterView.options.app; + + filter.fetch({ + success: function () { + app.state.set('search', false); + app.favoriteFilter.clear({ silent: true }); + app.favoriteFilter.set(filter.toJSON()); + } + }); + + this.options.filterView.hideDetails(); + }, + + + serializeData: function () { + return _.extend({}, this.model.toJSON(), { + items: _.sortBy(this.model.get('choices'), function(item) { + return item.name.toLowerCase(); + }) + }); + } + }); + + + var IssuesFavoriteFilterView = FavoriteFiltersModule.FavoriteFilterView.extend({ + + initialize: function () { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: IssuesDetailsFavoriteFilterView + }); + + this.listenTo(window.SS.appState, 'change:favorites', this.updateFavorites); + }, + + + updateFavorites: function () { + this.model.set('choices', window.SS.appState.get('favorites')); + this.render(); + } + }); + + + var IssuesRouter = Backbone.Router.extend({ + + routes: { + '': 'emptyQuery', + ':query': 'index' + }, + + + initialize: function (options) { + this.app = options.app; + }, + + + parseQuery: function (query, separator) { + return (query || '').split(separator || '|').map(function (t) { + var tokens = t.split('='); + return { + key: tokens[0], + value: decodeURIComponent(tokens[1]) + } + }); + }, + + + emptyQuery: function () { + this.navigate('resolved=false', { trigger: true, replace: true }); + }, + + + index: function (query) { + var params = this.parseQuery(query); + + var idObj = _.findWhere(params, { key: 'id' }); + if (idObj) { + var that = this, + f = this.app.favoriteFilter; + this.app.canSave = false; + f.set('id', idObj.value); + f.fetch({ + success: function () { + params = _.extend({}, that.parseQuery(f.get('query')), params); + that.loadResults(params); + } + }); + } else { + this.loadResults(params); + } + }, + + + loadResults: function (params) { + this.app.filterBarView.restoreFromQuery(params); + this.app.restoreSorting(params); + this.app.fetchFirstPage(); + } + + }); + + + /* + * Export public classes + */ + + return { + AppState: AppState, + Issue: Issue, + Issues: Issues, + FavoriteFilter: FavoriteFilter, + FavoriteFilters: FavoriteFilters, + IssueView: IssueView, + IssuesView: IssuesView, + IssuesActionsView: IssuesActionsView, + IssuesFilterBarView: IssuesFilterBarView, + IssuesHeaderView: IssuesHeaderView, + IssuesFavoriteFilterView: IssuesFavoriteFilterView, + IssueDetailView: IssueDetailView, + IssuesRouter: IssuesRouter + }; + + }); diff --git a/sonar-server/src/main/js/measures/app.js b/sonar-server/src/main/js/measures/app.js new file mode 100644 index 00000000000..830a6cf4eb4 --- /dev/null +++ b/sonar-server/src/main/js/measures/app.js @@ -0,0 +1,195 @@ +requirejs.config({ + baseUrl: baseUrl + '/js', + + paths: { + 'backbone': 'third-party/backbone', + 'backbone.marionette': 'third-party/backbone.marionette', + 'handlebars': 'third-party/handlebars' + }, + + shim: { + 'backbone.marionette': { + deps: ['backbone'], + exports: 'Marionette' + }, + 'backbone': { + exports: 'Backbone' + }, + 'handlebars': { + exports: 'Handlebars' + } + } + +}); + +requirejs( + [ + 'backbone', 'backbone.marionette', + 'navigator/filters/filter-bar', + 'navigator/filters/base-filters', + 'navigator/filters/checkbox-filters', + 'navigator/filters/choice-filters', + 'navigator/filters/ajax-select-filters', + 'navigator/filters/favorite-filters', + 'navigator/filters/range-filters', + 'navigator/filters/string-filters', + 'navigator/filters/metric-filters' + ], + function (Backbone, Marionette, FilterBar, BaseFilters, CheckboxFilterView, ChoiceFilters, AjaxSelectFilters, + FavoriteFilters, RangeFilters, StringFilterView, MetricFilterView) { + + var NavigatorApp = new Marionette.Application(); + + + NavigatorApp.addRegions({ + filtersRegion: '.navigator-filters' + }); + + + NavigatorApp.addInitializer(function () { + this.filters = new BaseFilters.Filters(); + + if (_.isObject(window.SS.favorites)) { + this.filters.add([ + new BaseFilters.Filter({ + type: FavoriteFilters.FavoriteFilterView, + enabled: true, + optional: false, + choices: window.SS.favorites, + favoriteUrl: '/measures/filter', + manageUrl: '/measures/manage' + })]); + } + + this.filters.add([ + new BaseFilters.Filter({ + name: window.SS.phrases.components, + property: 'qualifiers[]', + type: ChoiceFilters.ChoiceFilterView, + enabled: true, + optional: false, + choices: window.SS.qualifiers, + defaultValue: window.SS.phrases.any + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.alert, + property: 'alertLevels[]', + type: ChoiceFilters.ChoiceFilterView, + enabled: false, + optional: true, + choices: { + 'error': window.SS.phrases.error, + 'warn': window.SS.phrases.warning, + 'ok': window.SS.phrases.ok + } + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.componentsOf, + property: 'base', + type: AjaxSelectFilters.ComponentFilterView, + multiple: false, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.favoritesOnly, + property: 'onFavourites', + type: CheckboxFilterView, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.date, + propertyFrom: 'fromDate', + propertyTo: 'toDate', + type: RangeFilters.DateRangeFilterView, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.keyContains, + property: 'keySearch', + type: StringFilterView, + enabled: false, + optional: true + }) + ]); + + this.filters.add([ + new BaseFilters.Filter({ + name: window.SS.phrases.lastAnalysis, + propertyFrom: 'ageMinDays', + propertyTo: 'ageMaxDays', + type: RangeFilters.RangeFilterView, + placeholder: window.SS.phrases.days, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.metric, + property: 'c3', + type: MetricFilterView, + metrics: window.SS.metrics, + periods: window.SS.metricPeriods, + operations: { 'eq': '=', 'lt': '<', 'lte': '≤', 'gt': '>', 'gte': '≥' }, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.metric, + property: 'c2', + type: MetricFilterView, + metrics: window.SS.metrics, + periods: window.SS.metricPeriods, + operations: { 'eq': '=', 'lt': '<', 'lte': '≤', 'gt': '>', 'gte': '≥' }, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.metric, + property: 'c1', + type: MetricFilterView, + metrics: window.SS.metrics, + periods: window.SS.metricPeriods, + operations: { 'eq': '=', 'lt': '<', 'lte': '≤', 'gt': '>', 'gte': '≥' }, + enabled: false, + optional: true + }), + + new BaseFilters.Filter({ + name: window.SS.phrases.nameContains, + property: 'nameSearch', + type: StringFilterView, + enabled: false, + optional: true + }) + ]); + + + this.filterBarView = new FilterBar({ + collection: this.filters, + extra: { + sort: '', + asc: false + } + }); + + + this.filtersRegion.show(this.filterBarView); + }); + + + NavigatorApp.start(); + if (window.queryParams) { + NavigatorApp.filterBarView.restoreFromQuery(window.queryParams); + } + + }); diff --git a/sonar-server/src/main/js/navigator/filters/action-plan-filters.js b/sonar-server/src/main/js/navigator/filters/action-plan-filters.js new file mode 100644 index 00000000000..7503e344d2a --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/action-plan-filters.js @@ -0,0 +1,104 @@ +define(['backbone', 'navigator/filters/base-filters', 'navigator/filters/choice-filters'], function (Backbone, BaseFilters, ChoiceFilters) { + + return ChoiceFilters.ChoiceFilterView.extend({ + + initialize: function() { + ChoiceFilters.ChoiceFilterView.prototype.initialize.apply(this, arguments); + this.projectFilter = this.model.get('projectFilter'); + this.listenTo(this.projectFilter, 'change:value', this.onChangeProjectFilter); + this.onChangeProjectFilter(); + }, + + + onChangeProjectFilter: function() { + var projects = this.projectFilter.get('value'); + if (_.isArray(projects) && projects.length === 1) { + return this.fetchActionPlans(projects[0]); + } else { + return this.makeInactive(); + } + }, + + + showDetails: function() { + if (!this.$el.is('.navigator-filter-inactive')) { + ChoiceFilters.ChoiceFilterView.prototype.showDetails.apply(this, arguments); + } + }, + + + makeActive: function() { + this.model.set({ + inactive: false, + title: '' + }); + this.model.trigger('change:enabled'); + this.$el.removeClass('navigator-filter-inactive').prop('title', ''); + }, + + + makeInactive: function() { + this.model.set({ + inactive: true, + title: window.SS.phrases.actionPlanNotAvailable + }); + this.model.trigger('change:enabled'); + this.choices.reset([]); + this.detailsView.updateLists(); + this.detailsView.updateValue(); + this.$el.addClass('navigator-filter-inactive') + .prop('title', window.SS.phrases.actionPlanNotAvailable); + }, + + + fetchActionPlans: function(project) { + var that = this; + return jQuery.ajax({ + url: baseUrl + '/api/action_plans/search', + type: 'GET', + data: { project: project } + }).done(function(r) { + var nonClosedActionPlans = + _.sortBy(_.reject(r.actionPlans, function(plan) { + return plan.status === 'CLOSED'; + }), 'name'); + + that.choices.reset(nonClosedActionPlans.map(function(plan) { + return { + id: plan.key, + text: plan.name, + category: plan.fDeadLine + } + })); + _.each(that.model.get('choices'), function(v, k) { + that.choices.add(new Backbone.Model({ id: k, text: v })); + }); + var value = that.model.get('value'); + _.each(value, function(v) { + var cModel = that.choices.findWhere({ id: v }); + cModel.set('checked', true); + }); + that.detailsView.updateValue(); + that.render(); + + that.makeActive(); + }); + }, + + + restore: function(value) { + if (_.isString(value)) { + value = value.split(','); + } + + if (this.choices && value.length > 0) { + this.model.set({ value: value, enabled: true }); + this.onChangeProjectFilter(); + } else { + this.clear(); + } + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/ajax-select-filters.js b/sonar-server/src/main/js/navigator/filters/ajax-select-filters.js new file mode 100644 index 00000000000..dd2e246d217 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/ajax-select-filters.js @@ -0,0 +1,465 @@ +define(['backbone', 'navigator/filters/base-filters', 'navigator/filters/choice-filters', 'common/handlebars-extensions'], function (Backbone, BaseFilters, ChoiceFilters) { + + var PAGE_SIZE = 100; + + + var Suggestions = Backbone.Collection.extend({ + comparator: 'checked', + + initialize: function() { + this.more = false; + this.page = 0; + }, + + + parse: function(r) { + this.more = r.more; + return r.results; + }, + + + fetch: function(options) { + this.data = _.extend({ + p: 1, + ps: PAGE_SIZE + }, options.data || {}); + + var settings = _.extend({}, options, { data: this.data }); + return Backbone.Collection.prototype.fetch.call(this, settings); + }, + + + fetchNextPage: function(options) { + if (this.more) { + this.data.p += 1; + var settings = _.extend({ remove: false }, options, { data: this.data }); + return this.fetch(settings); + } + return false; + } + + }); + + + + var UserSuggestions = Suggestions.extend({ + + url: function() { + return baseUrl + '/api/users/search?f=s2'; + } + + }); + + + + var ProjectSuggestions = Suggestions.extend({ + + url: function() { + return baseUrl + '/api/resources/search?f=s2&q=TRK&display_key=true'; + } + + }); + + + + var ComponentSuggestions = Suggestions.extend({ + + url: function() { + return baseUrl + '/api/resources/search?f=s2&qp=supportsGlobalDashboards&display_key=true'; + }, + + parse: function(r) { + this.more = r.more; + + // If results are divided into categories + if (r.results.length > 0 && r.results[0].children) { + var results = []; + _.each(r.results, function(category) { + _.each(category.children, function(child) { + child.category = category.text; + results.push(child); + }); + }); + return results; + } else { + return r.results; + } + } + + }); + + + + var AjaxSelectDetailsFilterView = ChoiceFilters.DetailsChoiceFilterView.extend({ + template: getTemplate('#ajax-select-filter-template'), + listTemplate: getTemplate('#choice-filter-template'), + + + render: function() { + ChoiceFilters.DetailsChoiceFilterView.prototype.render.apply(this, arguments); + + var that = this, + keyup = function(e) { + if (e.keyCode !== 38 && e.keyCode !== 40) { + that.search(); + } + }, + debouncedKeyup = _.debounce(keyup, 250), + scroll = function() { that.scroll(); }, + throttledScroll = _.throttle(scroll, 1000); + + this.$('.navigator-filter-search input') + .off('keyup keydown') + .on('keyup', debouncedKeyup) + .on('keydown', this.keydown); + + this.$('.choices') + .off('scroll') + .on('scroll', throttledScroll); + }, + + + search: function() { + var that = this; + this.query = this.$('.navigator-filter-search input').val(); + if (this.query.length > 1) { + this.$el.addClass('fetching'); + var selected = that.options.filterView.getSelected(); + this.options.filterView.choices.fetch({ + data: { + s: this.query, + ps: PAGE_SIZE + }, + success: function() { + selected.forEach(function(item) { + that.options.filterView.choices.unshift(item); + }); + _.each(that.model.get('choices'), function(v, k) { + that.options.filterView.choices.add(new Backbone.Model({ id: k, text: v })); + }); + that.updateLists(); + that.$el.removeClass('fetching'); + } + }); + } else { + this.resetChoices(); + this.updateLists(); + } + }, + + + scroll: function() { + var that = this, + el = this.$('.choices'), + scrollBottom = el.scrollTop() >= el[0].scrollHeight - el.outerHeight(); + + if (scrollBottom) { + this.options.filterView.choices.fetchNextPage().done(function() { + that.updateLists(); + }); + } + }, + + + keydown: function(e) { + if (_([37, 38, 39, 40, 13]).indexOf(e.keyCode) !== -1) { + e.preventDefault(); + } + }, + + + resetChoices: function() { + var that = this; + this.options.filterView.choices.reset(this.options.filterView.choices.filter(function(item) { + return item.get('checked'); + })); + _.each(this.model.get('choices'), function(v, k) { + that.options.filterView.choices.add(new Backbone.Model({ id: k, text: v })); + }); + }, + + + onShow: function() { + ChoiceFilters.DetailsChoiceFilterView.prototype.onShow.apply(this, arguments); + this.resetChoices(); + this.render(); + this.$('.navigator-filter-search input').focus(); + } + + }); + + + + var AjaxSelectFilterView = ChoiceFilters.ChoiceFilterView.extend({ + + initialize: function() { + ChoiceFilters.ChoiceFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + }, + + + isDefaultValue: function() { + return this.getSelected().length === 0; + }, + + + renderInput: function() { + var value = this.model.get('value') || [], + input = $j('') + .prop('name', this.model.get('property')) + .prop('type', 'hidden') + .css('display', 'none') + .val(value.join()); + input.appendTo(this.$el); + }, + + + restoreFromQuery: function(q) { + var param = _.findWhere(q, { key: this.model.get('property') }); + + if (this.model.get('choices')) { + _.each(this.model.get('choices'), function(v, k) { + if (k[0] === '!') { + var x = _.findWhere(q, { key: k.substr(1) }); + if (x) { + if (!param) { + param = { value: k }; + } else { + param.value += ',' + k; + } + } + } + }); + } + + if (param && param.value) { + this.model.set('enabled', true); + this.restore(param.value, param); + } else { + this.clear(); + } + }, + + + restore: function(value, param) { + var that = this; + if (_.isString(value)) { + value = value.split(','); + } + + if (this.choices && value.length > 0) { + this.model.set({ value: value, enabled: true }); + + var opposite = _.filter(value, function(item) { + return item[0] === '!'; + }); + opposite.forEach(function(item) { + that.choices.add(new Backbone.Model({ + id: item, + text: that.model.get('choices')[item], + checked: true + })); + }); + + value = _.reject(value, function(item) { + return item[0] === '!'; + }); + if (_.isArray(param.text) && param.text.length === value.length) { + this.restoreFromText(value, param.text); + } else { + this.restoreByRequests(value); + } + } else { + this.clear(); + } + }, + + + restoreFromText: function(value, text) { + var that = this; + _.each(value, function(v, i) { + that.choices.add(new Backbone.Model({ + id: v, + text: text[i], + checked: true + })); + }); + this.onRestore(value); + }, + + + restoreByRequests: function(value) { + var that = this, + requests = _.map(value, function(v) { + return that.createRequest(v); + }); + + $j.when.apply($j, requests).done(function () { + that.onRestore(value); + }); + }, + + + onRestore: function() { + this.detailsView.updateLists(); + this.renderBase(); + }, + + + clear: function() { + this.model.unset('value'); + if (this.choices) { + this.choices.reset([]); + } + this.render(); + }, + + + createRequest: function() {} + + }); + + + + var ComponentFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + AjaxSelectFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + this.choices = new ComponentSuggestions(); + }, + + + createRequest: function(v) { + var that = this; + return $j + .ajax({ + url: baseUrl + '/api/resources', + type: 'GET', + data: { resource: v } + }) + .done(function (r) { + that.selection.add(new Backbone.Model({ + id: r[0].key, + text: r[0].name + })); + }); + } + + }); + + + + var ProjectFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + + this.choices = new ProjectSuggestions(); + }, + + + createRequest: function(v) { + var that = this; + return $j + .ajax({ + url: baseUrl + '/api/resources', + type: 'GET', + data: { resource: v } + }) + .done(function (r) { + that.choices.add(new Backbone.Model({ + id: r[0].key, + text: r[0].name, + checked: true + })); + }); + } + + }); + + + + var AssigneeFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + + this.choices = new UserSuggestions(); + }, + + createRequest: function(v) { + var that = this; + return $j + .ajax({ + url: baseUrl + '/api/users/search', + type: 'GET', + data: { logins: v } + }) + .done(function (r) { + that.choices.add(new Backbone.Model({ + id: r.users[0].login, + text: r.users[0].name + ' (' + r.users[0].login + ')', + checked: true + })); + }); + } + + }); + + + + var ReporterFilterView = AjaxSelectFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectDetailsFilterView + }); + + this.selection = new UserSuggestions(); + this.choices = new UserSuggestions(); + }, + + + createRequest: function(v) { + var that = this; + return $j + .ajax({ + url: baseUrl + '/api/users/search', + type: 'GET', + data: { logins: v } + }) + .done(function (r) { + that.choices.add(new Backbone.Model({ + id: r.users[0].login, + text: r.users[0].name + ' (' + r.users[0].login + ')', + checked: true + })); + }); + } + + }); + + + + /* + * Export public classes + */ + + return { + Suggestions: Suggestions, + AjaxSelectDetailsFilterView: AjaxSelectDetailsFilterView, + AjaxSelectFilterView: AjaxSelectFilterView, + ProjectFilterView: ProjectFilterView, + ComponentFilterView: ComponentFilterView, + AssigneeFilterView: AssigneeFilterView, + ReporterFilterView: ReporterFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/base-filters.js b/sonar-server/src/main/js/navigator/filters/base-filters.js new file mode 100644 index 00000000000..e07a3517bfe --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/base-filters.js @@ -0,0 +1,225 @@ +define(['backbone', 'backbone.marionette', 'common/handlebars-extensions'], function (Backbone, Marionette) { + + var Filter = Backbone.Model.extend({ + + defaults: { + enabled: true, + optional: false, + multiple: true, + placeholder: '' + } + + }); + + + + var Filters = Backbone.Collection.extend({ + model: Filter + }); + + + + var DetailsFilterView = Marionette.ItemView.extend({ + template: getTemplate('#base-details-filter-template'), + className: 'navigator-filter-details', + + + initialize: function() { + this.$el.on('click', function(e) { + e.stopPropagation(); + }); + }, + + + onShow: function() {}, + onHide: function() {} + }); + + + + var BaseFilterView = Marionette.ItemView.extend({ + template: getTemplate('#base-filter-template'), + className: 'navigator-filter', + + + events: function() { + return { + 'click': 'toggleDetails', + 'click .navigator-filter-disable': 'disable' + }; + }, + + + modelEvents: { + 'change:enabled': 'focus', + 'change:value': 'renderBase', + + // for more criteria filter + 'change:filters': 'render' + }, + + + initialize: function(options) { + Marionette.ItemView.prototype.initialize.apply(this, arguments); + + var detailsView = (options && options.detailsView) || DetailsFilterView; + this.detailsView = new detailsView({ + model: this.model, + filterView: this + }); + + this.model.view = this; + }, + + + attachDetailsView: function() { + this.detailsView.$el.detach().appendTo($j('body')); + }, + + + render: function() { + this.renderBase(); + + this.attachDetailsView(); + this.detailsView.render(); + + this.$el.toggleClass( + 'navigator-filter-disabled', + !this.model.get('enabled')); + + this.$el.toggleClass( + 'navigator-filter-optional', + this.model.get('optional')); + }, + + + renderBase: function() { + Marionette.ItemView.prototype.render.apply(this, arguments); + this.renderInput(); + }, + + + renderInput: function() {}, + + + focus: function() { + this.render(); +// this.showDetails(); + }, + + + toggleDetails: function(e) { + e.stopPropagation(); + if (this.$el.hasClass('active')) { + this.hideDetails(); + } else { + this.showDetails(); + } + }, + + + showDetails: function() { + this.registerShowedDetails(); + + var top = this.$el.offset().top + this.$el.outerHeight() - 1, + left = this.$el.offset().left; + + this.detailsView.$el.css({ top: top, left: left }).addClass('active'); + this.$el.addClass('active'); + this.detailsView.onShow(); + }, + + + registerShowedDetails: function() { + this.options.filterBarView.hideDetails(); + this.options.filterBarView.showedView = this; + }, + + + hideDetails: function() { + this.detailsView.$el.removeClass('active'); + this.$el.removeClass('active'); + this.detailsView.onHide(); + }, + + + isActive: function() { + return this.$el.is('.active'); + }, + + + renderValue: function() { + return this.model.get('value') || 'unset'; + }, + + + isDefaultValue: function() { + return true; + }, + + + restoreFromQuery: function(q) { + var param = _.findWhere(q, { key: this.model.get('property') }); + if (param && param.value) { + this.model.set('enabled', true); + this.restore(param.value, param); + } else { + this.clear(); + } + }, + + + restore: function(value) { + this.model.set({ value: value }, { silent: true }); + this.renderBase(); + }, + + + clear: function() { + this.model.unset('value'); + }, + + + disable: function(e) { + e.stopPropagation(); + this.hideDetails(); + this.options.filterBarView.hideDetails(); + this.model.set({ + enabled: false, + value: null + }); + }, + + + formatValue: function() { + var q = {}; + if (this.model.has('property') && this.model.has('value') && this.model.get('value')) { + q[this.model.get('property')] = this.model.get('value'); + } + return q; + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + value: this.renderValue(), + defaultValue: this.isDefaultValue() + }); + } + + }); + + + + /* + * Export public classes + */ + + return { + Filter: Filter, + Filters: Filters, + BaseFilterView: BaseFilterView, + DetailsFilterView: DetailsFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/checkbox-filters.js b/sonar-server/src/main/js/navigator/filters/checkbox-filters.js new file mode 100644 index 00000000000..025ce9dc649 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/checkbox-filters.js @@ -0,0 +1,50 @@ +define(['backbone', 'backbone.marionette', 'navigator/filters/base-filters', 'common/handlebars-extensions'], function (Backbone, Marionette, BaseFilters) { + + return BaseFilters.BaseFilterView.extend({ + template: getTemplate('#checkbox-filter-template'), + className: 'navigator-filter navigator-filter-inline', + + + events: function() { + return { + 'click .navigator-filter-disable': 'disable' + }; + }, + + + showDetails: function() {}, + + + renderInput: function() { + if (this.model.get('enabled')) { + $j('') + .prop('name', this.model.get('property')) + .prop('type', 'checkbox') + .prop('value', 'true') + .prop('checked', true) + .css('display', 'none') + .appendTo(this.$el); + } + }, + + + renderValue: function() { + return this.model.get('value') || false; + }, + + + isDefaultValue: function() { + return false; + }, + + + restore: function(value) { + this.model.set({ + value: value, + enabled: true + }); + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/choice-filters.js b/sonar-server/src/main/js/navigator/filters/choice-filters.js new file mode 100644 index 00000000000..cf74a21562e --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/choice-filters.js @@ -0,0 +1,363 @@ +define(['handlebars', 'navigator/filters/base-filters', 'common/handlebars-extensions'], function (Handlebars, BaseFilters) { + + var DetailsChoiceFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#choice-filter-template'), + itemTemplate: getTemplate('#choice-filter-item-template'), + + + events: function() { + return { + 'change input[type=checkbox]': 'onCheck' + }; + }, + + + render: function() { + BaseFilters.DetailsFilterView.prototype.render.apply(this, arguments); + this.updateLists(); + }, + + + renderList: function(collection, selector) { + var that = this, + container = this.$(selector); + + container.empty().toggleClass('hidden', collection.length === 0); + collection.each(function(item) { + container.append(that.itemTemplate(item.toJSON())); + }); + }, + + + updateLists: function() { + var choices = new Backbone.Collection(this.options.filterView.choices.reject(function(item) { + return item.get('id')[0] === '!'; + })), + opposite = new Backbone.Collection(this.options.filterView.choices.filter(function(item) { + return item.get('id')[0] === '!'; + })); + + this.renderList(choices, '.choices'); + this.renderList(opposite, '.opposite'); + + var current = this.currentChoice || 0; + this.updateCurrent(current); + }, + + + onCheck: function(e) { + var checkbox = jQuery(e.target), + id = checkbox.val(), + checked = checkbox.prop('checked'); + + if (this.model.get('multiple')) { + if (checkbox.closest('.opposite').length > 0) { + this.options.filterView.choices.reject(function(item) { + return item.get('id')[0] === '!' + }).forEach(function(item) { + item.set('checked', false); + }); + } else { + this.options.filterView.choices.filter(function(item) { + return item.get('id')[0] === '!' + }).forEach(function(item) { + item.set('checked', false); + }); + } + } else { + this.options.filterView.choices.each(function(item) { + item.set('checked', false); + }); + } + + this.options.filterView.choices.get(id).set('checked', checked); + this.updateValue(); + this.updateLists(); + }, + + + updateValue: function() { + this.model.set('value', this.options.filterView.getSelected().map(function(m) { + return m.get('id'); + })); + }, + + + updateCurrent: function(index) { + this.currentChoice = index; + this.$('label').removeClass('current') + .eq(this.currentChoice).addClass('current'); + }, + + + onShow: function() { + this.bindedOnKeyDown = _.bind(this.onKeyDown, this); + $j('body').on('keydown', this.bindedOnKeyDown); + }, + + + onHide: function() { + $j('body').off('keydown', this.bindedOnKeyDown); + }, + + + onKeyDown: function(e) { + switch (e.keyCode) { + case 38: + e.preventDefault(); + this.selectPrevChoice(); + break; + case 40: + e.preventDefault(); + this.selectNextChoice(); + break; + case 13: + e.preventDefault(); + this.selectCurrent(); + break; + } + }, + + + selectNextChoice: function() { + if (this.$('label').length > this.currentChoice + 1) { + this.updateCurrent(this.currentChoice + 1); + this.scrollNext(); + } + }, + + + scrollNext: function() { + var currentLabel = this.$('label').eq(this.currentChoice); + if (currentLabel.length > 0) { + var list = currentLabel.closest('ul'), + labelPos = currentLabel.offset().top - list.offset().top + list.scrollTop(), + deltaScroll = labelPos - list.height() + currentLabel.outerHeight(); + + if (deltaScroll > 0) { + list.scrollTop(deltaScroll); + } + } + }, + + + selectPrevChoice: function() { + if (this.currentChoice > 0) { + this.updateCurrent(this.currentChoice - 1); + this.scrollPrev(); + } + }, + + + scrollPrev: function() { + var currentLabel = this.$('label').eq(this.currentChoice); + if (currentLabel.length > 0) { + var list = currentLabel.closest('ul'), + labelPos = currentLabel.offset().top - list.offset().top; + + if (labelPos < 0) { + list.scrollTop(list.scrollTop() + labelPos); + } + } + }, + + + selectCurrent: function() { + this.$('label').eq(this.currentChoice).click(); + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + choices: new Backbone.Collection(this.options.filterView.choices.reject(function(item) { + return item.get('id')[0] === '!'; + })).toJSON(), + opposite: new Backbone.Collection(this.options.filterView.choices.filter(function(item) { + return item.get('id')[0] === '!'; + })).toJSON() + }); + } + + }); + + + + var ChoiceFilterView = BaseFilters.BaseFilterView.extend({ + + initialize: function(options) { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: (options && options.detailsView) ? options.detailsView : DetailsChoiceFilterView + }); + + var index = 0, + icons = this.model.get('choiceIcons'); + + this.choices = new Backbone.Collection( + _.map(this.model.get('choices'), function(value, key) { + var model = new Backbone.Model({ + id: key, + text: value, + checked: false, + index: index++ + }); + + if (icons && icons[key]) { + model.set('icon', icons[key]); + } + + return model; + }), { comparator: 'index' } + ); + }, + + + getSelected: function() { + return this.choices.filter(function(m) { + return m.get('checked'); + }); + }, + + + renderInput: function() { + var input = $j('') + .prop('name', that.model.get('property') + '_' + key) + .prop('type', 'hidden') + .css('display', 'none') + .val(v) + .appendTo(that.$el); + }); + } + }, + + + isDefaultValue: function() { + var value = this.model.get('value'); + if (!_.isObject(value)) { + return true; + } + return !(value.metric && value.op && value.val); + }, + + + restoreFromQuery: function(q) { + var that = this, + value = {}; + _.each(['metric', 'period', 'op', 'val'], function(p) { + var property = that.model.get('property') + '_' + p, + pValue = _.findWhere(q, { key: property }); + + if (pValue && pValue.value) { + value[p] = pValue.value; + } + }); + + if (value && value.metric && value.op && value.val) { + this.model.set({ + value: value, + enabled: true + }); + } + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/more-criteria-filters.js b/sonar-server/src/main/js/navigator/filters/more-criteria-filters.js new file mode 100644 index 00000000000..755c4bbfb42 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/more-criteria-filters.js @@ -0,0 +1,70 @@ +define(['navigator/filters/base-filters', 'navigator/filters/choice-filters', 'common/handlebars-extensions'], function (BaseFilters, ChoiceFilters) { + + var DetailsMoreCriteriaFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#more-criteria-details-filter-template'), + + + events: { + 'click label[data-id]:not(.inactive)': 'enableFilter' + }, + + + enableFilter: function(e) { + var id = $j(e.target).data('id'); + this.model.view.options.filterBarView.enableFilter(id); + this.model.view.hideDetails(); + }, + + + serializeData: function() { + var filters = this.model.get('filters').map(function(filter) { + return _.extend(filter.toJSON(), { id: filter.cid }); + }), + uniqueFilters = _.unique(filters, function(filter) { + return filter.name; + }); + return _.extend(this.model.toJSON(), { filters: uniqueFilters }); + } + + }); + + + + var MoreCriteriaFilterView = ChoiceFilters.ChoiceFilterView.extend({ + template: getTemplate('#more-criteria-filter-template'), + className: 'navigator-filter navigator-filter-more-criteria', + + + initialize: function() { + ChoiceFilters.ChoiceFilterView.prototype.initialize.call(this, { + detailsView: DetailsMoreCriteriaFilterView + }); + }, + + + renderValue: function() { + return ''; + }, + + + renderInput: function() {}, + + + isDefaultValue: function() { + return false; + } + + }); + + + + /* + * Export public classes + */ + + return { + DetailsMoreCriteriaFilterView: DetailsMoreCriteriaFilterView, + MoreCriteriaFilterView: MoreCriteriaFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/range-filters.js b/sonar-server/src/main/js/navigator/filters/range-filters.js new file mode 100644 index 00000000000..910d37c7fa5 --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/range-filters.js @@ -0,0 +1,195 @@ +define(['navigator/filters/base-filters', 'common/handlebars-extensions'], function (BaseFilters) { + + var DetailsRangeFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#range-filter-template'), + + + events: { + 'change input': 'change' + }, + + + change: function() { + var value = {}, + valueFrom = this.$('input').eq(0).val(), + valueTo = this.$('input').eq(1).val(); + + if (valueFrom.length > 0) { + value[this.model.get('propertyFrom')] = valueFrom; + } + + if (valueTo.length > 0) { + value[this.model.get('propertyTo')] = valueTo; + } + + this.model.set('value', value); + }, + + + populateInputs: function() { + var value = this.model.get('value'), + propertyFrom = this.model.get('propertyFrom'), + propertyTo = this.model.get('propertyTo'), + valueFrom = _.isObject(value) && value[propertyFrom], + valueTo = _.isObject(value) && value[propertyTo]; + + this.$('input').eq(0).val(valueFrom || ''); + this.$('input').eq(1).val(valueTo || ''); + }, + + + onShow: function() { + this.$(':input:first').focus(); + } + + }); + + + + var RangeFilterView = BaseFilters.BaseFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsRangeFilterView + }); + }, + + + renderValue: function() { + if (!this.isDefaultValue()) { + var value = _.values(this.model.get('value')); + return value.join(' — '); + } else { + return window.SS.phrases.any; + } + }, + + + renderInput: function() { + var value = this.model.get('value'), + propertyFrom = this.model.get('propertyFrom'), + propertyTo = this.model.get('propertyTo'), + valueFrom = _.isObject(value) && value[propertyFrom], + valueTo = _.isObject(value) && value[propertyTo]; + + $j('') + .prop('name', propertyFrom) + .prop('type', 'hidden') + .css('display', 'none') + .val(valueFrom || '') + .appendTo(this.$el); + + $j('') + .prop('name', propertyTo) + .prop('type', 'hidden') + .css('display', 'none') + .val(valueTo || '') + .appendTo(this.$el); + }, + + + isDefaultValue: function() { + var value = this.model.get('value'), + propertyFrom = this.model.get('propertyFrom'), + propertyTo = this.model.get('propertyTo'), + valueFrom = _.isObject(value) && value[propertyFrom], + valueTo = _.isObject(value) && value[propertyTo]; + + return !valueFrom && !valueTo; + }, + + + restoreFromQuery: function(q) { + var paramFrom = _.findWhere(q, { key: this.model.get('propertyFrom') }), + paramTo = _.findWhere(q, { key: this.model.get('propertyTo') }), + value = {}; + + if ((paramFrom && paramFrom.value) || (paramTo && paramTo.value)) { + if (paramFrom && paramFrom.value) { + value[this.model.get('propertyFrom')] = paramFrom.value; + } + + if (paramTo && paramTo.value) { + value[this.model.get('propertyTo')] = paramTo.value; + } + + this.model.set({ + value: value, + enabled: true + }); + + this.detailsView.populateInputs(); + } + }, + + + restore: function(value) { + if (this.choices && this.selection && value.length > 0) { + var that = this; + this.choices.add(this.selection.models); + this.selection.reset([]); + + _.each(value, function(v) { + var cModel = that.choices.findWhere({ id: v }); + + if (cModel) { + that.selection.add(cModel); + that.choices.remove(cModel); + } + }); + + this.detailsView.updateLists(); + + this.model.set({ + value: value, + enabled: true + }); + } + }, + + + formatValue: function() { + return this.model.get('value'); + }, + + + clear: function() { + this.model.unset('value'); + this.detailsView.render(); + } + + }); + + + + var DateRangeFilterView = RangeFilterView.extend({ + + render: function() { + RangeFilterView.prototype.render.apply(this, arguments); + this.detailsView.$('input').prop('placeholder', '1970-01-31'); + }, + + + renderValue: function() { + if (!this.isDefaultValue()) { + var value = _.values(this.model.get('value')); + return value.join(' — '); + } else { + return window.SS.phrases.anytime; + } + } + + }); + + + + /* + * Export public classes + */ + + return { + RangeFilterView: RangeFilterView, + DateRangeFilterView: DateRangeFilterView + }; + +}); diff --git a/sonar-server/src/main/js/navigator/filters/read-only-filters.js b/sonar-server/src/main/js/navigator/filters/read-only-filters.js new file mode 100644 index 00000000000..5a203e5214f --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/read-only-filters.js @@ -0,0 +1,25 @@ +define(['backbone', 'navigator/filters/base-filters'], function (Backbone, BaseFilters) { + + return BaseFilters.BaseFilterView.extend({ + className: 'navigator-filter navigator-filter-read-only', + + + events: { + 'click .navigator-filter-disable': 'disable' + }, + + + isDefaultValue: function() { + return false; + }, + + + renderValue: function() { + var value = this.model.get('value'), + format = this.model.get('format'); + return value ? (format ? format(value) : value) : ''; + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/rule-filters.js b/sonar-server/src/main/js/navigator/filters/rule-filters.js new file mode 100644 index 00000000000..aed4b8d894a --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/rule-filters.js @@ -0,0 +1,50 @@ +define(['backbone', 'navigator/filters/base-filters', 'navigator/filters/ajax-select-filters'], function (Backbone, BaseFilters, AjaxSelectFilters) { + + var RuleSuggestions = AjaxSelectFilters.Suggestions.extend({ + + url: function() { + return baseUrl + '/api/rules/list'; + }, + + + parse: function(r) { + this.more = r.more; + return r.results.map(function(r) { + return { id: r.key, text: r.name, category: r.language }; + }); + } + + }); + + return AjaxSelectFilters.AjaxSelectFilterView.extend({ + + initialize: function() { + AjaxSelectFilters.AjaxSelectFilterView.prototype.initialize.call(this, { + detailsView: AjaxSelectFilters.AjaxSelectDetailsFilterView + }); + + this.choices = new RuleSuggestions(); + }, + + + createRequest: function(v) { + var that = this; + return jQuery + .ajax({ + url: baseUrl + '/api/rules/show', + type: 'GET', + data: { key: v } + }) + .done(function (r) { + that.choices.add(new Backbone.Model({ + id: r.rule.key, + text: r.rule.name, + category: r.rule.language, + checked: true + })); + }); + } + + }); + +}); diff --git a/sonar-server/src/main/js/navigator/filters/string-filters.js b/sonar-server/src/main/js/navigator/filters/string-filters.js new file mode 100644 index 00000000000..2e1278f350f --- /dev/null +++ b/sonar-server/src/main/js/navigator/filters/string-filters.js @@ -0,0 +1,77 @@ +define(['navigator/filters/base-filters', 'common/handlebars-extensions'], function (BaseFilters) { + + var DetailsStringFilterView = BaseFilters.DetailsFilterView.extend({ + template: getTemplate('#string-filter-template'), + + + events: { + 'change input': 'change' + }, + + + change: function(e) { + this.model.set('value', $j(e.target).val()); + }, + + + onShow: function() { + BaseFilters.DetailsFilterView.prototype.onShow.apply(this, arguments); + this.$(':input').focus(); + }, + + + serializeData: function() { + return _.extend({}, this.model.toJSON(), { + value: this.model.get('value') || '' + }); + } + + }); + + + + return BaseFilters.BaseFilterView.extend({ + + initialize: function() { + BaseFilters.BaseFilterView.prototype.initialize.call(this, { + detailsView: DetailsStringFilterView + }); + }, + + + renderValue: function() { + return this.isDefaultValue() ? '—' : this.model.get('value'); + }, + + + renderInput: function() { + $j('') + .prop('name', this.model.get('property')) + .prop('type', 'hidden') + .css('display', 'none') + .val(this.model.get('value') || '') + .appendTo(this.$el); + }, + + + isDefaultValue: function() { + return !this.model.get('value'); + }, + + + restore: function(value) { + this.model.set({ + value: value, + enabled: true + }); + }, + + + clear: function() { + this.model.unset('value'); + this.detailsView.render(); + } + + }); + +}); diff --git a/sonar-server/src/main/js/recent-history.js b/sonar-server/src/main/js/recent-history.js new file mode 100644 index 00000000000..e371603c963 --- /dev/null +++ b/sonar-server/src/main/js/recent-history.js @@ -0,0 +1,65 @@ +window.Sonar = {}; + +Sonar.RecentHistory = function () { +}; + +Sonar.RecentHistory.prototype.getRecentHistory = function () { + var sonarHistory = localStorage.getItem("sonar_recent_history"); + if (sonarHistory == null) { + sonarHistory = []; + } else { + sonarHistory = JSON.parse(sonarHistory); + } + return sonarHistory; +}; + +Sonar.RecentHistory.prototype.clear = function () { + localStorage.clear(); +}; + +Sonar.RecentHistory.prototype.add = function (resourceKey, resourceName, iconPath) { + var sonarHistory = this.getRecentHistory(); + + if (resourceKey !== '') { + var newEntry = {'key': resourceKey, 'name': resourceName, 'iconPath': iconPath}; + // removes the element of the array if it exists + for (var i = 0; i < sonarHistory.length; i++) { + var item = sonarHistory[i]; + if (item['key'] === resourceKey) { + sonarHistory.splice(i, 1); + break; + } + } + // then add it to the beginning of the array + sonarHistory.unshift(newEntry); + // and finally slice the array to keep only 10 elements + sonarHistory = sonarHistory.slice(0, 10); + + localStorage.setItem("sonar_recent_history", JSON.stringify(sonarHistory)); + } +}; + +Sonar.RecentHistory.prototype.populateRecentHistoryPanel = function () { + var historyLinksList = $j('#recent-history-list'); + historyLinksList.empty(); + + var recentHistory = this.getRecentHistory(); + if (recentHistory.length === 0) { + $j("#recent-history").hide(); + } else { + recentHistory.forEach(function (resource) { + historyLinksList.append('
  • ' + + resource['name'] + + '
  • '); + }); + $j("#recent-history").show(); + } +}; diff --git a/sonar-server/src/main/js/require.js b/sonar-server/src/main/js/require.js new file mode 100644 index 00000000000..84d1d678cf4 --- /dev/null +++ b/sonar-server/src/main/js/require.js @@ -0,0 +1,36 @@ +/* + RequireJS 2.1.10 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. + Available via the MIT or new BSD license. + see: http://github.com/jrburke/requirejs for details +*/ +var requirejs,require,define; +(function(ca){function G(b){return"[object Function]"===N.call(b)}function H(b){return"[object Array]"===N.call(b)}function v(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(G(c)){if(this.events.error&&this.map.isDefine||h.onError!==da)try{f=i.execCb(b,c,e,f)}catch(d){a=d}else f=i.execCb(b,c,e,f);this.map.isDefine&&void 0===f&&((e=this.module)?f=e.exports:this.usingExports&& +(f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=c;this.exports=f;if(this.map.isDefine&&!this.ignore&&(p[b]=f,h.onResourceLoad))h.onResourceLoad(i,this.map,this.depMaps);y(b);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a= +this.map,b=a.id,d=m(a.prefix);this.depMaps.push(d);r(d,"defined",t(this,function(f){var d,g;g=j(ba,this.map.id);var J=this.map.name,u=this.map.parentMap?this.map.parentMap.name:null,p=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(J=f.normalize(J,function(a){return c(a,u,!0)})||""),f=m(a.prefix+"!"+J,this.map.parentMap),r(f,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),g=j(k,f.id)){this.depMaps.push(f); +if(this.events.error)g.on("error",t(this,function(a){this.emit("error",a)}));g.enable()}}else g?(this.map.url=i.nameToUrl(g),this.load()):(d=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),d.error=t(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(k,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),d.fromText=t(this,function(f,c){var g=a.name,J=m(g),k=O;c&&(f=c);k&&(O=!1);q(J);s(l.config,b)&&(l.config[g]=l.config[b]);try{h.exec(f)}catch(j){return w(C("fromtexteval", +"fromText eval for "+b+" failed: "+j,j,[b]))}k&&(O=!0);this.depMaps.push(J);i.completeLoad(g);p([g],d)}),f.load(a.name,p,d,l))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){W[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,t(this,function(a,b){var c,f;if("string"===typeof a){a=m(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=j(K,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;r(a,"defined",t(this,function(a){this.defineDep(b, +a);this.check()}));this.errback&&r(a,"error",t(this,this.errback))}c=a.id;f=k[c];!s(K,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,t(this,function(a){var b=j(k,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:l,contextName:b,registry:k,defined:p,urlFetched:T,defQueue:A,Module:$,makeModuleMap:m, +nextTick:h.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=l.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(l[b]||(l[b]={}),V(l[b],a,!0,!0)):l[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(ba[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),l.shim=b);a.packages&&v(a.packages,function(a){var b, +a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(l.paths[b]=a.location);l.pkgs[b]=a.name+"/"+(a.main||"main").replace(ja,"").replace(R,"")});B(k,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=m(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ca,arguments));return b||a.exports&&ea(a.exports)}},makeRequire:function(a,e){function g(f,c,d){var j,l;e.enableBuildCallback&&(c&&G(c))&&(c.__requireJsBuild= +!0);if("string"===typeof f){if(G(c))return w(C("requireargs","Invalid require call"),d);if(a&&s(K,f))return K[f](k[a.id]);if(h.get)return h.get(i,f,a,g);j=m(f,a,!1,!0);j=j.id;return!s(p,j)?w(C("notloaded",'Module name "'+j+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[j]}M();i.nextTick(function(){M();l=q(m(null,a));l.skipMap=e.skipMap;l.init(f,c,d,{enabled:!0});D()});return g}e=e||{};V(g,{isBrowser:z,toUrl:function(b){var e,d=b.lastIndexOf("."),g=b.split("/")[0];if(-1!== +d&&(!("."===g||".."===g)||1g.attachEvent.toString().indexOf("[native code"))&&!Z?(O=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)): +(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1)),g.src=d,M=g,D?y.insertBefore(g,D):y.appendChild(g),M=null,g;if(fa)try{importScripts(d),b.completeLoad(c)}catch(j){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,j,[c]))}};z&&!r.skipDataMain&&U(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(L=b.getAttribute("data-main"))return q=L,r.baseUrl||(E=q.split("/"),q=E.pop(),Q=E.length?E.join("/")+"/":"./",r.baseUrl= +Q),q=q.replace(R,""),h.jsExtRegExp.test(q)&&(q=L),r.deps=r.deps?r.deps.concat(q):[q],!0});define=function(b,c,d){var g,h;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(la,"").replace(ma,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(O){if(!(g=M))P&&"interactive"===P.readyState||U(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),g=P;g&&(b|| +(b=g.getAttribute("data-requiremodule")),h=F[g.getAttribute("data-requirecontext")])}(h?h.defQueue:S).push([b,c,d])};define.amd={jQuery:!0};h.exec=function(b){return eval(b)};h(r)}})(this); diff --git a/sonar-server/src/main/js/resource.js b/sonar-server/src/main/js/resource.js new file mode 100644 index 00000000000..04165488070 --- /dev/null +++ b/sonar-server/src/main/js/resource.js @@ -0,0 +1,59 @@ +/* + Functions used in resource viewers + */ + +function loadResourceViewer(resourceId, tab, display_title, period, elt) { + if (display_title == undefined) { + display_title = true; + } + + var url = baseUrl + '/resource/index/' + resourceId + '?tab=' + tab + '&display_title=' + display_title + + '&period=' + period; + openAccordionItem(url, elt, true); + + return false; +} + +// Display GWT component +function loadGWT(gwtId, resourceId, resourceKey, resourceName, resourceScope, resourceQualifier, resourceLanguage) { + config["resource"] = [ + {"id":resourceId, "key":resourceKey, "name":resourceName, "scope":resourceScope, "qualifier":resourceQualifier, + "lang":resourceLanguage} + ]; + config["resource_key"] = resourceId; + modules[gwtId](); +} + +/* + Functions used in tests viewer + */ +function expandTests(index, elt){ + expandAccordionItem(elt); + var parent = $j(elt).closest('.test_name_'+index); + parent.find(".test_expandLink_"+ index).hide(); + parent.find(".test_collapseLink_"+ index).show(); + parent.next(".tests_viewer .test_message_"+ index).show(); +} + +function collapseTests(index, elt){ + expandAccordionItem(elt); + var parent = $j(elt).closest('.test_name_'+index); + parent.find(".test_collapseLink_"+ index).hide(); + parent.find(".test_expandLink_"+ index).show(); + parent.next(".tests_viewer .test_message_"+ index).hide(); +} + +/* Source decoration functions */ +function highlight_usages(event){ + var isAlreadyHighlighted = false; + var selectedElementClasses = $j(this).attr("class").split(" "); + if(selectedElementClasses.indexOf("highlighted") !== -1) { + isAlreadyHighlighted = true; + } + $j("#" + event.data.id + " span.highlighted").removeClass("highlighted"); + + if(!isAlreadyHighlighted) { + var selectedClass = selectedElementClasses[0]; + $j("#" + event.data.id + " span." + selectedClass).addClass("highlighted"); + } +} diff --git a/sonar-server/src/main/js/select2-jquery-ui-fix.js b/sonar-server/src/main/js/select2-jquery-ui-fix.js new file mode 100644 index 00000000000..edaa4899a82 --- /dev/null +++ b/sonar-server/src/main/js/select2-jquery-ui-fix.js @@ -0,0 +1,9 @@ +/* https://github.com/ivaynberg/select2/issues/1246 */ + +;(function($) { + + $.ui.dialog.prototype._allowInteraction = function(e) { + return !!$(e.target).closest('.ui-dialog, .ui-datepicker, .select2-drop').length; + }; + +})(jQuery); diff --git a/sonar-server/src/main/js/sortable.js b/sonar-server/src/main/js/sortable.js new file mode 100644 index 00000000000..6f3eae3147d --- /dev/null +++ b/sonar-server/src/main/js/sortable.js @@ -0,0 +1,74 @@ +(function($) { + + function _stripe(rows) { + rows.each(function(index) { + $(this).toggleClass('rowodd', index % 2 === 0); + $(this).toggleClass('roweven', index % 2 !== 0); + }); + } + + + function _getValue(cell) { + return cell.attr('x') || $.trim(cell.text()) || ''; + } + + + function _sort(container, rows, cellIndex, order) { + var sortArray = rows.map(function(index) { + var cell = $(this).find('td').eq(cellIndex); + return { index: index, value: _getValue(cell) }; + }).get(); + + Array.prototype.sort.call(sortArray, function(a, b) { + if (isNaN(a.value) || isNaN(a.value)) { + return order * (a.value > b.value ? 1 : -1); + } else { + return order * (a.value - b.value); + } + }); + + rows.detach(); + var newRows = jQuery(); + sortArray.forEach(function(a) { + var row = rows.eq(a.index); + row.appendTo(container); + newRows = newRows.add(row); + }); + + _stripe(newRows); + } + + + function _markSorted(headCells, cell, asc) { + headCells.removeClass('sortasc sortdesc'); + cell.toggleClass('sortasc', asc); + cell.toggleClass('sortdesc', !asc); + } + + + $.fn.sortable = function() { + return $(this).each(function() { + var thead = $(this).find('thead'), + tbody = $(this).find('tbody'), + headCells = thead.find('tr:last th'), + rows = tbody.find('tr'); + + headCells.filter(':not(.nosort)').addClass('sortcol'); + headCells.filter(':not(.nosort)').on('click', function() { + var asc = !$(this).is('.sortasc'); + _markSorted(headCells, $(this), asc); + _sort(tbody, rows, headCells.index($(this)), asc ? 1 : -1); + }); + + var sortFirst = headCells.filter('[class^=sortfirst],[class*=sortfirst]'); + if (sortFirst.length > 0) { + var asc = sortFirst.is('.sortfirstasc'); + _markSorted(headCells, sortFirst, asc); + _sort(tbody, rows, headCells.index(sortFirst), asc ? 1 : -1); + } else { + _stripe(rows); + } + }); + }; + +})(jQuery); diff --git a/sonar-server/src/main/js/tests/main.js b/sonar-server/src/main/js/tests/main.js new file mode 100644 index 00000000000..c012e4777e2 --- /dev/null +++ b/sonar-server/src/main/js/tests/main.js @@ -0,0 +1,45 @@ +window.__karma__.loaded = function() {}; + +var tests = []; +for (var file in window.__karma__.files) { + if (window.__karma__.files.hasOwnProperty(file)) { + if (/Spec\.js$/.test(file)) { + tests.push(file); + } + } +} + + +requirejs.config({ + baseUrl: '/base', + + paths: { + 'backbone': 'third-party/backbone', + 'backbone.marionette': 'third-party/backbone.marionette', + 'handlebars': 'third-party/handlebars', + 'moment': 'third-party/moment' + }, + + shim: { + 'backbone.marionette': { + deps: ['backbone'], + exports: 'Marionette' + }, + 'backbone': { + exports: 'Backbone' + }, + 'handlebars': { + exports: 'Handlebars' + }, + 'moment': { + exports: 'moment' + } + }, + + // ask Require.js to load these files (all our tests) + deps: tests, + + // start test run, once Require.js is done + callback: window.__karma__.start + +}); diff --git a/sonar-server/src/main/js/tests/navigator/filters/BaseFilterViewSpec.js b/sonar-server/src/main/js/tests/navigator/filters/BaseFilterViewSpec.js new file mode 100644 index 00000000000..b1a0ab15c4f --- /dev/null +++ b/sonar-server/src/main/js/tests/navigator/filters/BaseFilterViewSpec.js @@ -0,0 +1,15 @@ +define(['navigator/filters/base-filters'], function(BaseFilters) { + + describe('BaseFilterView', function() { + + it('initializes', function() { + var baseFilterView = new BaseFilters.BaseFilterView({ + model: new BaseFilters.Filter() + }); + expect(baseFilterView.detailsView).toBeDefined(); + expect(baseFilterView.detailsView.options.filterView).toBe(baseFilterView); + }); + + }); + +}); diff --git a/sonar-server/src/main/js/tests/navigator/filters/ChoiceFilterViewSpec.js b/sonar-server/src/main/js/tests/navigator/filters/ChoiceFilterViewSpec.js new file mode 100644 index 00000000000..3b905a9f88f --- /dev/null +++ b/sonar-server/src/main/js/tests/navigator/filters/ChoiceFilterViewSpec.js @@ -0,0 +1,39 @@ +define(['navigator/filters/base-filters', 'navigator/filters/choice-filters'], function(BaseFilters, ChoiceFilters) { + + describe('BaseFilterView', function() { + var choices, choiceFilter, choiceFilterView; + + beforeEach(function() { + choices = { + 'ONE': 'one', + 'TWO': 'two', + 'THREE': 'three', + '!OPPOSITE': 'opposite' + }; + + choiceFilter = new BaseFilters.Filter({ + name: 'Choice Filter Name', + property: 'choiceFilterProperty', + type: ChoiceFilters.ChoiceFilterView, + enabled: true, + optional: false, + choices: choices + }); + + choiceFilterView = new ChoiceFilters.ChoiceFilterView({ + model: choiceFilter + }); + }); + + it('creates choices', function() { + expect(choiceFilterView.choices).toBeDefined(); + expect(choiceFilterView.choices.length).toBe(Object.keys(choices).length); + }); + + it('does not have selected by default', function() { + expect(choiceFilterView.getSelected().length).toBe(0); + }); + + }); + +}); diff --git a/sonar-server/src/main/js/third-party/backbone.js b/sonar-server/src/main/js/third-party/backbone.js new file mode 100644 index 00000000000..f7783c2c199 --- /dev/null +++ b/sonar-server/src/main/js/third-party/backbone.js @@ -0,0 +1,1581 @@ +// Backbone.js 1.1.0 + +// (c) 2010-2011 Jeremy Ashkenas, DocumentCloud Inc. +// (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(){ + + // Initial Setup + // ------------- + + // Save a reference to the global object (`window` in the browser, `exports` + // on the server). + var root = this; + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create local references to array methods we'll want to use later. + var array = []; + var push = array.push; + var slice = array.slice; + var splice = array.splice; + + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both the browser and the server. + var Backbone; + if (typeof exports !== 'undefined') { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '1.1.0'; + + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); + + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // --------------- + + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback + // functions to an event; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on: function(name, callback, context) { + if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; + this._events || (this._events = {}); + var events = this._events[name] || (this._events[name] = []); + events.push({callback: callback, context: context, ctx: context || this}); + return this; + }, + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. + once: function(name, callback, context) { + if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; + var self = this; + var once = _.once(function() { + self.off(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + return this.on(name, once, context); + }, + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off: function(name, callback, context) { + var retain, ev, events, names, i, l, j, k; + if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; + if (!name && !callback && !context) { + this._events = {}; + return this; + } + names = name ? [name] : _.keys(this._events); + for (i = 0, l = names.length; i < l; i++) { + name = names[i]; + if (events = this._events[name]) { + this._events[name] = retain = []; + if (callback || context) { + for (j = 0, k = events.length; j < k; j++) { + ev = events[j]; + if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || + (context && context !== ev.context)) { + retain.push(ev); + } + } + } + if (!retain.length) delete this._events[name]; + } + } + + return this; + }, + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(name) { + if (!this._events) return this; + var args = slice.call(arguments, 1); + if (!eventsApi(this, 'trigger', name, args)) return this; + var events = this._events[name]; + var allEvents = this._events.all; + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); + return this; + }, + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + var remove = !name && !callback; + if (!callback && typeof name === 'object') callback = this; + if (obj) (listeningTo = {})[obj._listenId] = obj; + for (var id in listeningTo) { + obj = listeningTo[id]; + obj.off(name, callback, this); + if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; + } + return this; + } + + }; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + + // Handle event maps. + if (typeof name === 'object') { + for (var key in name) { + obj[action].apply(obj, [key, name[key]].concat(rest)); + } + return false; + } + + // Handle space separated event names. + if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [names[i]].concat(rest)); + } + return false; + } + + return true; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + } + }; + + var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; + + // Inversion-of-control versions of `on` and `once`. Tell *this* object to + // listen to an event in another object ... keeping track of what it's + // listening to. + _.each(listenMethods, function(implementation, method) { + Events[method] = function(obj, name, callback) { + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + listeningTo[id] = obj; + if (!callback && typeof name === 'object') callback = this; + obj[implementation](name, callback, this); + return this; + }; + }); + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId('c'); + this.attributes = {}; + if (options.collection) this.collection = options.collection; + if (options.parse) attrs = this.parse(attrs, options) || {}; + attrs = _.defaults({}, attrs, _.result(this, 'defaults')); + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false; + var old = this._changing ? this._previousAttributes : this.attributes; + for (var attr in diff) { + if (_.isEqual(old[attr], (val = diff[attr]))) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overridden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + if (!model.set(model.parse(resp, options), options)) return false; + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + var attrs, method, xhr, attributes = this.attributes; + + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options = _.extend({validate: true}, options); + + // If we're not waiting and attributes exist, save acts as + // `set(attr).save(null, opts)` with validation. Otherwise, check if + // the model will be valid when the attributes, if any, are set. + if (attrs && !options.wait) { + if (!this.set(attrs, options)) return false; + } else { + if (!this._validate(attrs, options)) return false; + } + + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } + if (success) success(model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); + if (method === 'patch') options.attrs = attrs; + xhr = this.sync(method, this, options); + + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + + var destroy = function() { + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (options.wait || model.isNew()) destroy(); + if (success) success(model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + if (this.isNew()) { + options.success(); + return false; + } + wrapError(this, options); + + var xhr = this.sync('delete', this, options); + if (!options.wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend(options || {}, { validate: true })); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options, {validationError: error})); + return false; + } + + }); + + // Underscore methods that we want to implement on the Model. + var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; + + // Mix in each Underscore method as a proxy to `Model#attributes`. + _.each(modelMethods, function(method) { + Model.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.attributes); + return _[method].apply(_, args); + }; + }); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analagous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, remove: false}; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model){ return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. + add: function(models, options) { + return this.set(models, _.extend({merge: false}, options, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + var singular = !_.isArray(models); + models = singular ? [models] : _.clone(models); + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = models[i] = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + this._removeReference(model); + } + return singular ? models[0] : models; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + options = _.defaults({}, options, setOptions); + if (options.parse) models = this.parse(models, options); + var singular = !_.isArray(models); + models = singular ? (models ? [models] : []) : _.clone(models); + var i, l, id, model, attrs, existing, sort; + var at = options.at; + var targetModel = this.model; + var sortable = this.comparator && (at == null) && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + var toAdd = [], toRemove = [], modelMap = {}; + var add = options.add, merge = options.merge, remove = options.remove; + var order = !sortable && add && remove ? [] : false; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + for (i = 0, l = models.length; i < l; i++) { + attrs = models[i]; + if (attrs instanceof Model) { + id = model = attrs; + } else { + id = attrs[targetModel.prototype.idAttribute]; + } + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + if (existing = this.get(id)) { + if (remove) modelMap[existing.cid] = true; + if (merge) { + attrs = attrs === model ? model.attributes : attrs; + if (options.parse) attrs = existing.parse(attrs, options); + existing.set(attrs, options); + if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + } + models[i] = existing; + + // If this is a new, valid model, push it to the `toAdd` list. + } else if (add) { + model = models[i] = this._prepareModel(attrs, options); + if (!model) continue; + toAdd.push(model); + + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on('all', this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + if (order) order.push(existing || model); + } + + // Remove nonexistent models if appropriate. + if (remove) { + for (i = 0, l = this.length; i < l; ++i) { + if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + } + if (toRemove.length) this.remove(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + if (toAdd.length || (order && order.length)) { + if (sortable) sort = true; + this.length += toAdd.length; + if (at != null) { + for (i = 0, l = toAdd.length; i < l; i++) { + this.models.splice(at + i, 0, toAdd[i]); + } + } else { + if (order) this.models.length = 0; + var orderedModels = order || toAdd; + for (i = 0, l = orderedModels.length; i < l; i++) { + this.models.push(orderedModels[i]); + } + } + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + // Unless silenced, it's time to fire all appropriate add/sort events. + if (!options.silent) { + for (i = 0, l = toAdd.length; i < l; i++) { + (model = toAdd[i]).trigger('add', model, this, options); + } + if (sort || (order && order.length)) this.trigger('sort', this, options); + } + + // Return the added (or merged) model (or models). + return singular ? models[0] : models; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + options.previousModels = this.models; + this._reset(); + models = this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return models; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + return this.add(model, _.extend({at: this.length}, options)); + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + return this.add(model, _.extend({at: 0}, options)); + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + + // Slice out a sub-array of models from the collection. + slice: function() { + return slice.apply(this.models, arguments); + }, + + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj]; + }, + + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return this[first ? 'find' : 'filter'](function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + // Run sort based on type of `comparator`. + if (_.isString(this.comparator) || this.comparator.length === 1) { + this.models = this.sortBy(this.comparator, this); + } else { + this.models.sort(_.bind(this.comparator, this)); + } + + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, 'get', attr); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success(collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(model, resp, options) { + if (options.wait) collection.add(model, options); + if (success) success(model, resp, options); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (attrs instanceof Model) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options = options ? _.clone(options) : {}; + options.collection = this; + var model = new this.model(attrs, options); + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model) { + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (model && event === 'change:' + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + if (model.id != null) this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + + }); + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', + 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', + 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', + 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', + 'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', + 'lastIndexOf', 'isEmpty', 'chain']; + + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.models); + return _[method].apply(_, args); + }; + }); + + // Underscore methods that take a property name as an argument. + var attributeMethods = ['groupBy', 'countBy', 'sortBy']; + + // Use attributes instead of properties. + _.each(attributeMethods, function(method) { + Collection.prototype[method] = function(value, context) { + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _[method](this.models, iterator, context); + }; + }); + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + options || (options = {}); + _.extend(this, _.pick(options, viewOptions)); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be merged as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be preferred to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this.$el.remove(); + this.stopListening(); + return this; + }, + + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, 'events')))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; + + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += '.delegateEvents' + this.cid; + if (selector === '') { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, + + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, 'el'), false); + } + } + + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === 'PATCH' && noXhrPatch) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + var noXhrPatch = typeof window !== 'undefined' && !!window.ActiveXObject && !(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + 'create': 'POST', + 'update': 'PUT', + 'patch': 'PATCH', + 'delete': 'DELETE', + 'read': 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + }); + return this; + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + return optional ? match : '([^\/]+)'; + }) + .replace(splatParam, '(.*?)'); + return new RegExp('^' + route + '$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, 'checkUrl'); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + + // Cached regex for stripping urls of hash and query. + var pathStripper = /[?#].*$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || !this._wantsHashChange || forcePushState) { + fragment = this.location.pathname; + var root = this.root.replace(trailingSlash, ''); + if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + if (oldIE && this._wantsHashChange) { + this.iframe = Backbone.$('