From d910309632d358d87f4a6988fb70e606661e7e47 Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Thu, 27 Nov 2014 17:14:37 +0100 Subject: [PATCH] Migrate source viewer to js --- .../popups/coverage-popup.coffee | 42 -- .../popups/duplication-popup.coffee | 55 -- .../popups/line-actions-popup.coffee | 40 -- .../main/coffee/source-viewer/source.coffee | 51 -- .../main/coffee/source-viewer/viewer.coffee | 360 ------------- .../js/source-viewer/popups/coverage-popup.js | 42 ++ .../source-viewer/popups/duplication-popup.js | 65 +++ .../popups/line-actions-popup.js | 41 ++ .../src/main/js/source-viewer/source.js | 62 +++ .../src/main/js/source-viewer/viewer.js | 489 ++++++++++++++++++ 10 files changed, 699 insertions(+), 548 deletions(-) delete mode 100644 server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee delete mode 100644 server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee delete mode 100644 server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee delete mode 100644 server/sonar-web/src/main/coffee/source-viewer/source.coffee delete mode 100644 server/sonar-web/src/main/coffee/source-viewer/viewer.coffee create mode 100644 server/sonar-web/src/main/js/source-viewer/popups/coverage-popup.js create mode 100644 server/sonar-web/src/main/js/source-viewer/popups/duplication-popup.js create mode 100644 server/sonar-web/src/main/js/source-viewer/popups/line-actions-popup.js create mode 100644 server/sonar-web/src/main/js/source-viewer/source.js create mode 100644 server/sonar-web/src/main/js/source-viewer/viewer.js diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee deleted file mode 100644 index 66b97c2addb..00000000000 --- a/server/sonar-web/src/main/coffee/source-viewer/popups/coverage-popup.coffee +++ /dev/null @@ -1,42 +0,0 @@ -define [ - 'backbone.marionette' - 'templates/source-viewer' - 'common/popup' - 'component-viewer/utils' -], ( - Marionette - Templates - Popup - utils -) -> - - $ = jQuery - - - class extends Popup - template: Templates['source-viewer-coverage-popup'] - - - events: - 'click a[data-key]': 'goToFile' - - - onRender: -> - super - @$('.bubble-popup-container').isolatedScroll() - - - goToFile: (e) -> - el = $(e.currentTarget) - key = el.data 'key' - method = el.data 'method' - files = @model.get 'files' - - - serializeData: -> - files = @model.get 'files' - tests = _.groupBy @model.get('tests'), '_ref' - testFiles = _.map tests, (testSet, fileRef) -> - file: files[fileRef] - tests: testSet - testFiles: testFiles diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee deleted file mode 100644 index 378ae8d8022..00000000000 --- a/server/sonar-web/src/main/coffee/source-viewer/popups/duplication-popup.coffee +++ /dev/null @@ -1,55 +0,0 @@ -define [ - 'backbone.marionette' - 'templates/component-viewer' - 'common/popup' - 'component-viewer/utils' -], ( - Marionette - Templates - Popup - utils -) -> - - $ = jQuery - - - class extends Popup - template: Templates['source-viewer-duplication-popup'] - - - events: - 'click a[data-key]': 'goToFile' - - - goToFile: (e) -> - key = $(e.currentTarget).data 'key' - line = $(e.currentTarget).data 'line' - files = @options.main.source.get('duplicationFiles') - options = @collection.map (item) -> - file = files[item.get('_ref')] - x = utils.splitLongName file.name - key: file.key - name: x.name - subname: x.dir - component: - projectName: file.projectName - subProjectName: file.subProjectName - active: file.key == key - options = _.uniq options, (item) -> item.key - - - serializeData: -> - files = @model.get 'duplicationFiles' - groupedBlocks = _.groupBy @collection.toJSON(), '_ref' - duplications = _.map groupedBlocks, (blocks, fileRef) -> - blocks: blocks - file: files[fileRef] - - duplications = _.sortBy duplications, (d) => - a = d.file.projectName != @model.get 'projectName' - b = d.file.subProjectName != @model.get 'subProjectName' - c = d.file.key != @model.get 'key' - '' + a + b + c - - component: @model.toJSON() - duplications: duplications diff --git a/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee b/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee deleted file mode 100644 index f1e96f0d454..00000000000 --- a/server/sonar-web/src/main/coffee/source-viewer/popups/line-actions-popup.coffee +++ /dev/null @@ -1,40 +0,0 @@ -define [ - 'backbone.marionette' - 'templates/source-viewer' - 'common/popup' - 'issue/manual-issue-view' -], ( - Marionette - Templates - Popup - ManualIssueView -) -> - - - class extends Popup - template: Templates['source-viewer-line-options-popup'] - - - events: - 'click .js-get-permalink': 'getPermalink' - 'click .js-add-manual-issue': 'addManualIssue' - - - getPermalink: (e) -> - e.preventDefault() - url = "#{baseUrl}/component/index#component=#{encodeURIComponent(@model.key())}&line=#{@options.line}" - windowParams = 'resizable=1,scrollbars=1,status=1' - window.open url, @model.get('name'), windowParams - - - addManualIssue: (e) -> - e.preventDefault() - line = @options.line - component = @model.key() - manualIssueView = new ManualIssueView - line: line - component: component - rules: @model.get 'manual_rules' - manualIssueView.render().$el.appendTo @options.row.find('.source-line-code') - manualIssueView.on 'add', (issue) => - @trigger 'onManualIssueAdded', issue diff --git a/server/sonar-web/src/main/coffee/source-viewer/source.coffee b/server/sonar-web/src/main/coffee/source-viewer/source.coffee deleted file mode 100644 index 7badb90d1ec..00000000000 --- a/server/sonar-web/src/main/coffee/source-viewer/source.coffee +++ /dev/null @@ -1,51 +0,0 @@ -define [ - 'backbone' -], ( - Backbone -) -> - - class extends Backbone.Model - idAttribute: 'uuid' - - defaults: -> - hasSource: false - hasCoverage: false - hasDuplications: false - hasSCM: false - - - key: -> - @get 'key' - - - addMeta: (meta) -> - source = @get 'source' - metaIdx = 0 - metaLine = meta[metaIdx] - source.forEach (line) -> - while metaLine? && line.line > metaLine.line - metaIdx++ - metaLine = meta[metaIdx] - if metaLine? && line.line == metaLine.line - _.extend line, metaLine - metaIdx++ - metaLine = meta[metaIdx] - @set source: source - - - addDuplications: (duplications) -> - source = @get 'source' - return unless source - source.forEach (line) -> - lineDuplications = [] - duplications.forEach (d, i) -> - duplicated = false - d.blocks.forEach (b) -> - if b._ref == '1' - lineFrom = b.from - lineTo = b.from + b.size - duplicated = true if line.line >= lineFrom && line.line <= lineTo - lineDuplications.push if duplicated then i + 1 else false - line.duplications = lineDuplications - @set source: source - diff --git a/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee b/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee deleted file mode 100644 index 17d94c95602..00000000000 --- a/server/sonar-web/src/main/coffee/source-viewer/viewer.coffee +++ /dev/null @@ -1,360 +0,0 @@ -define [ - 'backbone.marionette' - 'templates/source-viewer' - 'source-viewer/source' - 'issue/models/issue' - 'issue/collections/issues' - 'issue/issue-view' - - 'source-viewer/popups/coverage-popup' - 'source-viewer/popups/duplication-popup' - 'source-viewer/popups/line-actions-popup' -], ( - Marionette - Templates - Source - Issue - Issues - IssueView - - CoveragePopupView - DuplicationPopupView - LineActionsPopupView -) -> - - $ = jQuery - - HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted' - - log = (message) -> - console.log 'Source Viewer:', message - - - class extends Marionette.ItemView - className: 'source' - template: Templates['source-viewer'] - - ISSUES_LIMIT: 100 - LINES_LIMIT: 1000 - LINES_AROUND: 500 - - - ui: - sourceBeforeSpinner: '.js-component-viewer-source-before' - sourceAfterSpinner: '.js-component-viewer-source-after' - - - events: -> - 'click .source-line-covered': 'showCoveragePopup' - 'click .source-line-partially-covered': 'showCoveragePopup' - - 'click .source-line-duplications': 'showDuplications' - 'click .source-line-duplications-extra': 'showDuplicationPopup' - - 'click .source-line-number[data-line-number]': 'highlightLine' - - - initialize: -> - @model = new Source() unless @model? - @issues = new Issues() - @issueViews = [] - @loadSourceBeforeThrottled = _.throttle @loadSourceBefore, 1000 - @loadSourceAfterThrottled = _.throttle @loadSourceAfter, 1000 - @scrollTimer = null - - - onRender: -> - log 'Render' - @renderIssues() - - - onClose: -> - @issueViews.forEach (view) -> view.close() - @issueViews = [] - - - open: (id, key) -> - @model.clear() - @model.set uuid: id, key: key - @requestComponent().done => - @requestSource() - .done => - @requestCoverage().done => - @requestDuplications().done => - @requestIssues().done => - @render() - @trigger 'loaded' - .fail => - @model.set source: [{ line: 0 }] - @requestIssues().done => - @render() - @trigger 'loaded' - - - requestComponent: -> - log 'Request component details...' - url = "#{baseUrl}/api/components/app" - options = key: @model.key() - $.get url, options, (data) => - @model.set data - log 'Component loaded' - - - linesLimit: -> - from: 1 - to: @LINES_LIMIT - - - requestSource: -> - log 'Request source...' - url = "#{baseUrl}/api/sources/lines" - options = _.extend { uuid: @model.id }, @linesLimit() - $.get url, options, (data) => - source = data.sources || [] - if source.length == 0 || (source.length > 0 && _.first(source).line == 1) - source.unshift { line: 0 } - firstLine = _.first(source).line - @model.set - source: source - hasSourceBefore: firstLine > 1 - hasSourceAfter: true - log 'Source loaded' - - - requestCoverage: -> - log 'Request coverage' - url = "#{baseUrl}/api/coverage/show" - options = key: @model.key() - $.get url, options, (data) => - hasCoverage = data? && data.coverage? - @model.set hasCoverage: hasCoverage - if hasCoverage - coverage = data.coverage.map (c) -> - status = 'partially-covered' - status = 'covered' if c[1] && c[3] == c[4] - status = 'uncovered' if !c[1] || c[4] == 0 - line: +c[0] - covered: status - else coverage = [] - @model.addMeta coverage - log 'Coverage loaded' - - - requestDuplications: -> - log 'Request duplications' - url = "#{baseUrl}/api/duplications/show" - options = key: @model.key() - $.get url, options, (data) => - hasDuplications = data? && data.duplications? - if hasDuplications - duplications = {} - data.duplications.forEach (d, i) -> - d.blocks.forEach (b) -> - if b._ref == '1' - lineFrom = b.from - lineTo = b.from + b.size - duplications[i] = true for i in [lineFrom..lineTo] - duplications = _.pairs(duplications).map (line) -> - line: +line[0] - duplicated: line[1] - else duplications = [] - @model.addMeta duplications - @model.addDuplications data.duplications - @model.set - duplications: data.duplications - duplicationFiles: data.files - log 'Duplications loaded' - - - requestIssues: -> - log 'Request issues' - options = data: - componentUuids: @model.id - extra_fields: 'actions,transitions,assigneeName,actionPlanName' - resolved: false - s: 'FILE_LINE' - asc: true - @issues.fetch(options).done => - @issues.reset @limitIssues @issues - @addIssuesPerLineMeta @issues - log 'Issues loaded' - - - addIssuesPerLineMeta: (issues) -> - lines = {} - issues.forEach (issue) -> - line = issue.get('line') || 0 - lines[line] = [] unless _.isArray lines[line] - lines[line].push issue.toJSON() - issuesPerLine = _.pairs(lines).map (line) -> - line: +line[0] - issues: line[1] - @model.addMeta issuesPerLine - - - limitIssues: (issues) -> - issues.first @ISSUES_LIMIT - - - renderIssues: -> - log 'Render issues' - @issues.forEach @renderIssue, @ - log 'Issues rendered' - - - renderIssue: (issue) -> - issueView = new IssueView - el: '#issue-' + issue.get('key') - model: issue - @issueViews.push issueView - issueView.render() - - - addIssue: (issue) -> - line = issue.get('line') || 0 - code = @$(".source-line-code[data-line-number=#{line}]") - issueList = code.find('.issue-list') - unless issueList.length > 0 - issueList = $('
') - code.append issueList - issueList.append "
" - @renderIssue issue - - - showSpinner: -> - hideSpinner: -> - resetShowBlocks: -> - - - showCoveragePopup: (e) -> - r = window.process.addBackgroundProcess() - e.stopPropagation() - $('body').click() - line = $(e.currentTarget).data 'line-number' - url = "#{baseUrl}/api/tests/test_cases" - options = - key: @model.key() - line: line - $.get url, options - .done (data) => - popup = new CoveragePopupView - model: new Backbone.Model data - triggerEl: $(e.currentTarget) - popup.render() - window.process.finishBackgroundProcess r - .fail -> - window.process.failBackgroundProcess r - - - showDuplications: -> - @$('.source-line-duplications').addClass 'hidden' - @$('.source-line-duplications-extra').removeClass 'hidden' - - - showDuplicationPopup: (e) -> - e.stopPropagation() - $('body').click() - index = $(e.currentTarget).data 'index' - line = $(e.currentTarget).data 'line-number' - blocks = @model.get('duplications')[index - 1].blocks - blocks = _.filter blocks, (b) -> - (b._ref != '1') || (b._ref == '1' && b.from > line) || (b._ref == '1' && b.from + b.size < line) - popup = new DuplicationPopupView - triggerEl: $(e.currentTarget) - model: @model - collection: new Backbone.Collection blocks - popup.render() - - - showLineActionsPopup: (e) -> - e.stopPropagation() - $('body').click() - line = $(e.currentTarget).data 'line-number' - popup = new LineActionsPopupView - triggerEl: $(e.currentTarget) - model: @model - line: line - row: $(e.currentTarget).closest '.source-line' - popup.on 'onManualIssueAdded', (data) => - @addIssue new Issue(data) - popup.render() - - - highlightLine: (e) -> - row = $(e.currentTarget).closest('.source-line') - highlighted = row.is ".#{HIGHLIGHTED_ROW_CLASS}" - @$(".#{HIGHLIGHTED_ROW_CLASS}").removeClass HIGHLIGHTED_ROW_CLASS - unless highlighted - row.addClass HIGHLIGHTED_ROW_CLASS - @showLineActionsPopup(e) - - - bindScrollEvents: -> - @$el.scrollParent().on 'scroll.source-viewer', (=> @onScroll()) - - - unbindScrollEvents: -> - @$el.scrollParent().off 'scroll.source-viewer' - - - disablePointerEvents: -> - clearTimeout @scrollTimer - $('body').addClass 'disabled-pointer-events' - @scrollTimer = setTimeout (-> $('body').removeClass 'disabled-pointer-events'), 250 - - - onScroll: -> - @disablePointerEvents() - - p = @$el.scrollParent() - p = $(window) if p.is(document) - pTopOffset = if p.offset()? then p.offset().top else 0 - if @model.get('hasSourceBefore') && (p.scrollTop() + pTopOffset <= @ui.sourceBeforeSpinner.offset().top) - @loadSourceBeforeThrottled() - - if @model.get('hasSourceAfter') && (p.scrollTop() + pTopOffset + p.height() >= @ui.sourceAfterSpinner.offset().top) - @loadSourceAfterThrottled() - - - loadSourceBefore: -> - @unbindScrollEvents() - source = @model.get 'source' - firstLine = _.first(source).line - url = "#{baseUrl}/api/sources/lines" - options = - uuid: @model.id - from: firstLine - @LINES_AROUND - to: firstLine - 1 - $.get url, options, (data) => - source = (data.sources || []).concat source - if source.length == 0 || (source.length > 0 && _.first(source).line == 1) - source.unshift { line: 0 } - @model.set - source: source - hasSourceBefore: data.sources.length == @LINES_AROUND - @render() - @scrollToLine firstLine - @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter') - - - loadSourceAfter: -> - @unbindScrollEvents() - source = @model.get 'source' - lastLine = _.last(source).line - url = "#{baseUrl}/api/sources/lines" - options = - uuid: @model.id - from: lastLine + 1 - to: lastLine + @LINES_AROUND - $.get url, options - .done (data) => - source = source.concat data.sources - @model.set - source: source - hasSourceAfter: data.sources.length == @LINES_AROUND - @render() - @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter') - .fail => - @model.set hasSourceAfter: false - @render() - @bindScrollEvents() if @model.get('hasSourceBefore') || @model.get('hasSourceAfter') diff --git a/server/sonar-web/src/main/js/source-viewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/source-viewer/popups/coverage-popup.js new file mode 100644 index 00000000000..f0a59152634 --- /dev/null +++ b/server/sonar-web/src/main/js/source-viewer/popups/coverage-popup.js @@ -0,0 +1,42 @@ +define([ + 'backbone.marionette', + 'templates/source-viewer', + 'common/popup', + 'component-viewer/utils' +], function (Marionette, Templates, Popup, utils) { + + var $ = jQuery; + + return Popup.extend({ + template: Templates['source-viewer-coverage-popup'], + + events: { + 'click a[data-key]': 'goToFile' + }, + + onRender: function () { + Popup.prototype.onRender.apply(this, arguments); + this.$('.bubble-popup-container').isolatedScroll(); + }, + + goToFile: function (e) { + // TODO Implement this + var el = $(e.currentTarget), + key = el.data('key'), + method = el.data('method'), + files = this.model.get('files'); + }, + + serializeData: function () { + var files = this.model.get('files'), + tests = _.groupBy(this.model.get('tests'), '_ref'), + testFiles = _.map(tests, function (testSet, fileRef) { + return { + file: files[fileRef], + tests: testSet + }; + }); + return { testFiles: testFiles }; + } + }); +}); diff --git a/server/sonar-web/src/main/js/source-viewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/source-viewer/popups/duplication-popup.js new file mode 100644 index 00000000000..40dbe8c0ebe --- /dev/null +++ b/server/sonar-web/src/main/js/source-viewer/popups/duplication-popup.js @@ -0,0 +1,65 @@ +define([ + 'backbone.marionette', + 'templates/component-viewer', + 'common/popup', + 'component-viewer/utils' +], function (Marionette, Templates, Popup, utils) { + + var $ = jQuery; + + return Popup.extend({ + template: Templates['source-viewer-duplication-popup'], + + events: { + 'click a[data-key]': 'goToFile' + }, + + goToFile: function (e) { + var key = $(e.currentTarget).data('key'), + line = $(e.currentTarget).data('line'), + files = this.options.main.source.get('duplicationFiles'), + options = this.collection.map(function (item) { + var file = files[item.get('_ref')], + x = utils.splitLongName(file.name); + return { + key: file.key, + name: x.name, + subname: x.dir, + component: { + projectName: file.projectName, + subProjectName: file.subProjectName + }, + active: file.key === key + }; + }); + return _.uniq(options, function (item) { + return item.key; + }); + }, + + serializeData: function () { + var duplications, files, groupedBlocks; + files = this.model.get('duplicationFiles'); + groupedBlocks = _.groupBy(this.collection.toJSON(), '_ref'); + duplications = _.map(groupedBlocks, function (blocks, fileRef) { + return { + blocks: blocks, + file: files[fileRef] + }; + }); + duplications = _.sortBy(duplications, (function (_this) { + return function (d) { + var a, b, c; + a = d.file.projectName !== _this.model.get('projectName'); + b = d.file.subProjectName !== _this.model.get('subProjectName'); + c = d.file.key !== _this.model.get('key'); + return '' + a + b + c; + }; + })(this)); + return { + component: this.model.toJSON(), + duplications: duplications + }; + } + }); +}); diff --git a/server/sonar-web/src/main/js/source-viewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/source-viewer/popups/line-actions-popup.js new file mode 100644 index 00000000000..3931272bc73 --- /dev/null +++ b/server/sonar-web/src/main/js/source-viewer/popups/line-actions-popup.js @@ -0,0 +1,41 @@ +define([ + 'backbone.marionette', + 'templates/source-viewer', + 'common/popup', + 'issue/manual-issue-view' +], function (Marionette, Templates, Popup, ManualIssueView) { + + return Popup.extend({ + template: Templates['source-viewer-line-options-popup'], + + events: { + 'click .js-get-permalink': 'getPermalink', + 'click .js-add-manual-issue': 'addManualIssue' + }, + + getPermalink: function (e) { + e.preventDefault(); + var url = baseUrl + '/component/index#component=' + + (encodeURIComponent(this.model.key())) + '&line=' + this.options.line, + windowParams = 'resizable=1,scrollbars=1,status=1'; + window.open(url, this.model.get('name'), windowParams); + }, + + addManualIssue: function (e) { + e.preventDefault(); + var that = this, + line = this.options.line, + component = this.model.key(), + manualIssueView = new ManualIssueView({ + line: line, + component: component, + rules: this.model.get('manual_rules') + }); + manualIssueView.render().$el.appendTo(this.options.row.find('.source-line-code')); + manualIssueView.on('add', function (issue) { + that.trigger('onManualIssueAdded', issue); + }); + } + }); +}); + diff --git a/server/sonar-web/src/main/js/source-viewer/source.js b/server/sonar-web/src/main/js/source-viewer/source.js new file mode 100644 index 00000000000..1ded0471cb7 --- /dev/null +++ b/server/sonar-web/src/main/js/source-viewer/source.js @@ -0,0 +1,62 @@ +define([ + 'backbone' +], function (Backbone) { + + return Backbone.Model.extend({ + idAttribute: 'uuid', + + defaults: function () { + return { + hasSource: false, + hasCoverage: false, + hasDuplications: false, + hasSCM: false + }; + }, + + key: function () { + return this.get('key'); + }, + + addMeta: function (meta) { + var source = this.get('source'), + metaIdx = 0, + metaLine = meta[metaIdx]; + source.forEach(function (line) { + while (metaLine != null && line.line > metaLine.line) { + metaLine = meta[++metaIdx]; + } + if (metaLine != null && line.line === metaLine.line) { + _.extend(line, metaLine); + metaLine = meta[++metaIdx]; + } + }); + this.set({ source: source }); + }, + + addDuplications: function (duplications) { + var source = this.get('source'); + if (source != null) { + source.forEach(function (line) { + var lineDuplications = []; + duplications.forEach(function (d, i) { + var duplicated = false; + d.blocks.forEach(function (b) { + if (b._ref === '1') { + var lineFrom = b.from, + lineTo = b.from + b.size; + if (line.line >= lineFrom && line.line <= lineTo) { + duplicated = true; + } + } + }); + lineDuplications.push(duplicated ? i + 1 : false); + }); + line.duplications = lineDuplications; + }); + } + this.set({ source: source }); + } + }); + +}); diff --git a/server/sonar-web/src/main/js/source-viewer/viewer.js b/server/sonar-web/src/main/js/source-viewer/viewer.js new file mode 100644 index 00000000000..3d9330faa25 --- /dev/null +++ b/server/sonar-web/src/main/js/source-viewer/viewer.js @@ -0,0 +1,489 @@ +define([ + 'backbone', + 'backbone.marionette', + 'templates/source-viewer', + 'source-viewer/source', + 'issue/models/issue', + 'issue/collections/issues', + 'issue/issue-view', + 'source-viewer/popups/coverage-popup', + 'source-viewer/popups/duplication-popup', + 'source-viewer/popups/line-actions-popup' +], function (Backbone, Marionette, Templates, Source, Issue, Issues, IssueView, CoveragePopupView, DuplicationPopupView, LineActionsPopupView) { + + var $ = jQuery, + HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted', + log = function (message) { + return console.log('Source Viewer:', message); + }; + + return Marionette.ItemView.extend({ + className: 'source', + template: Templates['source-viewer'], + + ISSUES_LIMIT: 100, + LINES_LIMIT: 1000, + LINES_AROUND: 500, + + ui: { + sourceBeforeSpinner: '.js-component-viewer-source-before', + sourceAfterSpinner: '.js-component-viewer-source-after' + }, + + events: function () { + return { + 'click .source-line-covered': 'showCoveragePopup', + 'click .source-line-partially-covered': 'showCoveragePopup', + 'click .source-line-duplications': 'showDuplications', + 'click .source-line-duplications-extra': 'showDuplicationPopup', + 'click .source-line-number[data-line-number]': 'highlightLine' + }; + }, + + initialize: function () { + if (this.model == null) { + this.model = new Source(); + } + this.issues = new Issues(); + this.issueViews = []; + this.loadSourceBeforeThrottled = _.throttle(this.loadSourceBefore, 1000); + this.loadSourceAfterThrottled = _.throttle(this.loadSourceAfter, 1000); + this.scrollTimer = null; + }, + + onRender: function () { + log('Render'); + this.renderIssues(); + return this; + }, + + onClose: function () { + this.issueViews.forEach(function (view) { + return view.close(); + }); + this.issueViews = []; + }, + + open: function (id, key) { + var that = this; + this.model.clear(); + this.model.set({ + uuid: id, + key: key + }); + this.requestComponent().done(function () { + that.requestSource() + .done(function () { + that.requestCoverage().done(function () { + that.requestDuplications().done(function () { + that.requestIssues().done(function () { + that.render(); + that.trigger('loaded'); + }); + }); + }); + }) + .fail(function () { + that.model.set({ + source: [ + { line: 0 } + ] + }); + that.requestIssues().done(function () { + that.render(); + that.trigger('loaded'); + }); + }); + }); + return this; + }, + + requestComponent: function () { + log('Request component details...'); + var that = this, + url = baseUrl + '/api/components/app', + options = { key: this.model.key() }; + return $.get(url, options).done(function (data) { + that.model.set(data); + }); + }, + + linesLimit: function () { + return { + from: 1, + to: this.LINES_LIMIT + }; + }, + + requestSource: function () { + var options, url; + log('Request source...'); + url = '' + baseUrl + '/api/sources/lines'; + options = _.extend({ + uuid: this.model.id + }, this.linesLimit()); + return $.get(url, options, (function (_this) { + return function (data) { + var firstLine, source; + source = data.sources || []; + if (source.length === 0 || (source.length > 0 && _.first(source).line === 1)) { + source.unshift({ + line: 0 + }); + } + firstLine = _.first(source).line; + _this.model.set({ + source: source, + hasSourceBefore: firstLine > 1, + hasSourceAfter: true + }); + return log('Source loaded'); + }; + })(this)); + }, + + requestCoverage: function () { + var options, url; + log('Request coverage'); + url = '' + baseUrl + '/api/coverage/show'; + options = { + key: this.model.key() + }; + return $.get(url, options, (function (_this) { + return function (data) { + var coverage, hasCoverage; + hasCoverage = (data != null) && (data.coverage != null); + _this.model.set({ + hasCoverage: hasCoverage + }); + if (hasCoverage) { + coverage = data.coverage.map(function (c) { + var status; + status = 'partially-covered'; + if (c[1] && c[3] === c[4]) { + status = 'covered'; + } + if (!c[1] || c[4] === 0) { + status = 'uncovered'; + } + return { + line: +c[0], + covered: status + }; + }); + } else { + coverage = []; + } + _this.model.addMeta(coverage); + return log('Coverage loaded'); + }; + })(this)); + }, + + requestDuplications: function () { + var options, url; + log('Request duplications'); + url = '' + baseUrl + '/api/duplications/show'; + options = { + key: this.model.key() + }; + return $.get(url, options, (function (_this) { + return function (data) { + var duplications, hasDuplications; + hasDuplications = (data != null) && (data.duplications != null); + if (hasDuplications) { + duplications = {}; + data.duplications.forEach(function (d, i) { + return d.blocks.forEach(function (b) { + var lineFrom, lineTo, _i, _results; + if (b._ref === '1') { + lineFrom = b.from; + lineTo = b.from + b.size; + _results = []; + for (i = _i = lineFrom; lineFrom <= lineTo ? _i <= lineTo : _i >= lineTo; i = lineFrom <= lineTo ? ++_i : --_i) { + _results.push(duplications[i] = true); + } + return _results; + } + }); + }); + duplications = _.pairs(duplications).map(function (line) { + return { + line: +line[0], + duplicated: line[1] + }; + }); + } else { + duplications = []; + } + _this.model.addMeta(duplications); + _this.model.addDuplications(data.duplications); + _this.model.set({ + duplications: data.duplications, + duplicationFiles: data.files + }); + return log('Duplications loaded'); + }; + })(this)); + }, + + requestIssues: function () { + var options; + log('Request issues'); + options = { + data: { + componentUuids: this.model.id, + extra_fields: 'actions,transitions,assigneeName,actionPlanName', + resolved: false, + s: 'FILE_LINE', + asc: true + } + }; + return this.issues.fetch(options).done((function (_this) { + return function () { + _this.issues.reset(_this.limitIssues(_this.issues)); + _this.addIssuesPerLineMeta(_this.issues); + return log('Issues loaded'); + }; + })(this)); + }, + + addIssuesPerLineMeta: function (issues) { + var issuesPerLine, lines; + lines = {}; + issues.forEach(function (issue) { + var line; + line = issue.get('line') || 0; + if (!_.isArray(lines[line])) { + lines[line] = []; + } + return lines[line].push(issue.toJSON()); + }); + issuesPerLine = _.pairs(lines).map(function (line) { + return { + line: +line[0], + issues: line[1] + }; + }); + return this.model.addMeta(issuesPerLine); + }, + + limitIssues: function (issues) { + return issues.first(this.ISSUES_LIMIT); + }, + + renderIssues: function () { + log('Render issues'); + this.issues.forEach(this.renderIssue, this); + return log('Issues rendered'); + }, + + renderIssue: function (issue) { + var issueView; + issueView = new IssueView({ + el: '#issue-' + issue.get('key'), + model: issue + }); + this.issueViews.push(issueView); + return issueView.render(); + }, + + addIssue: function (issue) { + var code, issueList, line; + line = issue.get('line') || 0; + code = this.$('.source-line-code[data-line-number=' + line + ']'); + issueList = code.find('.issue-list'); + if (issueList.length === 0) { + issueList = $('
'); + code.append(issueList); + } + issueList.append('
'); + return this.renderIssue(issue); + }, + + showCoveragePopup: function (e) { + var line, options, r, url; + r = window.process.addBackgroundProcess(); + e.stopPropagation(); + $('body').click(); + line = $(e.currentTarget).data('line-number'); + url = '' + baseUrl + '/api/tests/test_cases'; + options = { + key: this.model.key(), + line: line + }; + return $.get(url, options).done((function (_this) { + return function (data) { + var popup; + popup = new CoveragePopupView({ + model: new Backbone.Model(data), + triggerEl: $(e.currentTarget) + }); + popup.render(); + return window.process.finishBackgroundProcess(r); + }; + })(this)).fail(function () { + return window.process.failBackgroundProcess(r); + }); + }, + + showDuplications: function () { + this.$('.source-line-duplications').addClass('hidden'); + return this.$('.source-line-duplications-extra').removeClass('hidden'); + }, + + showDuplicationPopup: function (e) { + var blocks, index, line, popup; + e.stopPropagation(); + $('body').click(); + index = $(e.currentTarget).data('index'); + line = $(e.currentTarget).data('line-number'); + blocks = this.model.get('duplications')[index - 1].blocks; + blocks = _.filter(blocks, function (b) { + return (b._ref !== '1') || (b._ref === '1' && b.from > line) || (b._ref === '1' && b.from + b.size < line); + }); + popup = new DuplicationPopupView({ + triggerEl: $(e.currentTarget), + model: this.model, + collection: new Backbone.Collection(blocks) + }); + return popup.render(); + }, + + showLineActionsPopup: function (e) { + var line, popup; + e.stopPropagation(); + $('body').click(); + line = $(e.currentTarget).data('line-number'); + popup = new LineActionsPopupView({ + triggerEl: $(e.currentTarget), + model: this.model, + line: line, + row: $(e.currentTarget).closest('.source-line') + }); + popup.on('onManualIssueAdded', (function (_this) { + return function (data) { + return _this.addIssue(new Issue(data)); + }; + })(this)); + return popup.render(); + }, + + highlightLine: function (e) { + var highlighted, row; + row = $(e.currentTarget).closest('.source-line'); + highlighted = row.is('.' + HIGHLIGHTED_ROW_CLASS); + this.$('.' + HIGHLIGHTED_ROW_CLASS).removeClass(HIGHLIGHTED_ROW_CLASS); + if (!highlighted) { + row.addClass(HIGHLIGHTED_ROW_CLASS); + return this.showLineActionsPopup(e); + } + }, + + bindScrollEvents: function () { + return this.$el.scrollParent().on('scroll.source-viewer', ((function (_this) { + return function () { + return _this.onScroll(); + }; + })(this))); + }, + + unbindScrollEvents: function () { + return this.$el.scrollParent().off('scroll.source-viewer'); + }, + + disablePointerEvents: function () { + clearTimeout(this.scrollTimer); + $('body').addClass('disabled-pointer-events'); + this.scrollTimer = setTimeout((function () { + return $('body').removeClass('disabled-pointer-events'); + }), 250); + }, + + onScroll: function () { + var p, pTopOffset; + this.disablePointerEvents(); + p = this.$el.scrollParent(); + if (p.is(document)) { + p = $(window); + } + pTopOffset = p.offset() != null ? p.offset().top : 0; + if (this.model.get('hasSourceBefore') && (p.scrollTop() + pTopOffset <= this.ui.sourceBeforeSpinner.offset().top)) { + this.loadSourceBeforeThrottled(); + } + if (this.model.get('hasSourceAfter') && (p.scrollTop() + pTopOffset + p.height() >= this.ui.sourceAfterSpinner.offset().top)) { + return this.loadSourceAfterThrottled(); + } + }, + + loadSourceBefore: function () { + var firstLine, options, source, url; + this.unbindScrollEvents(); + source = this.model.get('source'); + firstLine = _.first(source).line; + url = '' + baseUrl + '/api/sources/lines'; + options = { + uuid: this.model.id, + from: firstLine - this.LINES_AROUND, + to: firstLine - 1 + }; + return $.get(url, options, (function (_this) { + return function (data) { + source = (data.sources || []).concat(source); + if (source.length === 0 || (source.length > 0 && _.first(source).line === 1)) { + source.unshift({ + line: 0 + }); + } + _this.model.set({ + source: source, + hasSourceBefore: data.sources.length === _this.LINES_AROUND + }); + _this.render(); + _this.scrollToLine(firstLine); + if (_this.model.get('hasSourceBefore') || _this.model.get('hasSourceAfter')) { + return _this.bindScrollEvents(); + } + }; + })(this)); + }, + + loadSourceAfter: function () { + var lastLine, options, source, url; + this.unbindScrollEvents(); + source = this.model.get('source'); + lastLine = _.last(source).line; + url = '' + baseUrl + '/api/sources/lines'; + options = { + uuid: this.model.id, + from: lastLine + 1, + to: lastLine + this.LINES_AROUND + }; + return $.get(url, options).done((function (_this) { + return function (data) { + source = source.concat(data.sources); + _this.model.set({ + source: source, + hasSourceAfter: data.sources.length === _this.LINES_AROUND + }); + _this.render(); + if (_this.model.get('hasSourceBefore') || _this.model.get('hasSourceAfter')) { + return _this.bindScrollEvents(); + } + }; + })(this)).fail((function (_this) { + return function () { + _this.model.set({ + hasSourceAfter: false + }); + _this.render(); + if (_this.model.get('hasSourceBefore') || _this.model.get('hasSourceAfter')) { + return _this.bindScrollEvents(); + } + }; + })(this)); + } + }); + +}); + -- 2.39.5