From b3f9be06398e8872cc64a966f99866b67e18c337 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Sat, 6 Jun 2015 23:15:32 -0400 Subject: Refactor and spec BlobView JS --- app/assets/javascripts/blob/blob.js.coffee | 257 +++++++++++++++++++++-------- app/views/shared/_file_highlight.html.haml | 4 +- spec/javascripts/blob/blob_spec.js.coffee | 169 +++++++++++++++++++ spec/javascripts/fixtures/blob.html.haml | 9 + 4 files changed, 365 insertions(+), 74 deletions(-) create mode 100644 spec/javascripts/blob/blob_spec.js.coffee create mode 100644 spec/javascripts/fixtures/blob.html.haml diff --git a/app/assets/javascripts/blob/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee index 37a175fdbc7..b7caae23f31 100644 --- a/app/assets/javascripts/blob/blob.js.coffee +++ b/app/assets/javascripts/blob/blob.js.coffee @@ -1,73 +1,186 @@ +# BlobView +# +# Handles single- and multi-line selection and highlight for blob views. +# +#= require jquery.scrollTo +# +# ### Example Markup +# +#
+#
+#
+# 1 +# 2 +# 3 +# 4 +# 5 +#
+#
+#         
+#           ...
+#           ...
+#           ...
+#           ...
+#           ...
+#         
+#       
+#
+#
class @BlobView - constructor: -> - # handle multi-line select - handleMultiSelect = (e) -> - [ first_line, last_line ] = parseSelectedLines() - [ line_number ] = parseSelectedLines($(this).attr("id")) - hash = "L#{line_number}" - - if e.shiftKey and not isNaN(first_line) and not isNaN(line_number) - if line_number < first_line - last_line = first_line - first_line = line_number - else - last_line = line_number - - hash = if first_line == last_line then "L#{first_line}" else "L#{first_line}-#{last_line}" - - setHash(hash) - e.preventDefault() - - # See if there are lines selected - # "#L12" and "#L34-56" supported - highlightBlobLines = (e) -> - [ first_line, last_line ] = parseSelectedLines() - - unless isNaN first_line - $("#tree-content-holder .highlight .line").removeClass("hll") - $("#LC#{line}").addClass("hll") for line in [first_line..last_line] - $.scrollTo("#L#{first_line}", offset: -50) unless e? - - # parse selected lines from hash - # always return first and last line (initialized to NaN) - parseSelectedLines = (str) -> - first_line = NaN - last_line = NaN - hash = str || window.location.hash - - if hash isnt "" - matches = hash.match(/\#?L(\d+)(\-(\d+))?/) - first_line = parseInt(matches?[1]) - last_line = parseInt(matches?[3]) - last_line = first_line if isNaN(last_line) - - [ first_line, last_line ] - - setHash = (hash) -> - hash = hash.replace(/^\#/, "") - nodes = $("#" + hash) - # if any nodes are using this id, they must be temporarily changed - # also, add a temporary div at the top of the screen to prevent scrolling - if nodes.length > 0 - scroll_top = $(document).scrollTop() - nodes.attr("id", "") - tmp = $("
") - .css({ position: "absolute", visibility: "hidden", top: scroll_top + "px" }) - .attr("id", hash) - .appendTo(document.body) - - window.location.hash = hash - - # restore the nodes - if nodes.length > 0 - tmp.remove() - nodes.attr("id", hash) - - # initialize multi-line select - $("#tree-content-holder .line-numbers a[id^=L]").on("click", handleMultiSelect) - - # Highlight the correct lines on load - highlightBlobLines() - - # Highlight the correct lines when the hash part of the URL changes - $(window).on("hashchange", highlightBlobLines) + # Internal copy of location.hash so we're not dependent on `location` in tests + @_hash = '' + + # Initialize a BlobView object + # + # hash - String URL hash for dependency injection in tests + constructor: (hash = location.hash) -> + @_hash = hash + + @bindEvents() + + unless hash == '' + range = @hashToRange(hash) + + unless isNaN(range[0]) + @highlightRange(range) + + # Scroll to the first highlighted line on initial load + # Offset -50 for the sticky top bar, and another -100 for some context + $.scrollTo("#L#{range[0]}", offset: -150) + + bindEvents: -> + $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler + + # While it may seem odd to bind to the mousedown event and then throw away + # the click event, there is a method to our madness. + # + # If not done this way, the line number anchor will sometimes keep its + # active state even when the event is cancelled, resulting in an ugly border + # around the link and/or a persisted underline text decoration. + + $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) -> + event.preventDefault() + + clickHandler: (event) => + event.preventDefault() + + lineNumber = $(event.target).data('line-number') + current = @hashToRange(@_hash) + + # Unhighlight previously highlighted lines + $('.hll').removeClass('hll') + + if isNaN(current[0]) or !event.shiftKey + # If there's no current selection, or there is but Shift wasn't held, + # treat this like a single-line selection. + @setHash(lineNumber) + @highlightLine(lineNumber) + else if event.shiftKey + if lineNumber < current[0] + range = [lineNumber, current[0]] + else + range = [current[0], lineNumber] + + @setHash(range[0], range[1]) + @highlightRange(range) + + # Convert a URL hash String into line numbers + # + # hash - Hash String + # + # Examples: + # + # hashToRange('#L5') # => [5, NaN] + # hashToRange('#L5-15') # => [5, 15] + # hashToRange('#foo') # => [NaN, NaN] + # + # Returns an Array + hashToRange: (hash) -> + first = parseInt(hash.replace(/^#L(\d+)/, '$1')) + last = parseInt(hash.replace(/^#L\d+-(\d+)/, '$1')) + + [first, last] + + # Highlight a single line + # + # lineNumber - Number to highlight. Must be parsable as an Integer. + # + # Returns undefined if lineNumber is not parsable as an Integer. + highlightLine: (lineNumber) -> + return if isNaN(parseInt(lineNumber)) + + $("#LC#{lineNumber}").addClass('hll') + + # Highlight all lines within a range + # + # range - An Array of starting and ending line numbers. + # + # Examples: + # + # # Highlight lines 5 through 15 + # highlightRange([5, 15]) + # + # # The first value is required, and must be a number + # highlightRange(['foo', 15]) # Invalid, returns undefined + # highlightRange([NaN, NaN]) # Invalid, returns undefined + # + # # The second value is optional; if omitted, only highlights the first line + # highlightRange([5, NaN]) # Valid + # + # Returns undefined if the first line is NaN. + highlightRange: (range) -> + return if isNaN(range[0]) + + if isNaN(range[1]) + @highlightLine(range[0]) + else + for lineNumber in [range[0]..range[1]] + @highlightLine(lineNumber) + + setHash: (firstLineNumber, lastLineNumber) => + return if isNaN(parseInt(firstLineNumber)) + + if isNaN(parseInt(lastLineNumber)) + hash = "#L#{firstLineNumber}" + else + hash = "#L#{firstLineNumber}-#{lastLineNumber}" + + @setHashWithoutScroll(hash) + + # Prevents the page from scrolling when `location.hash` is set + # + # This is accomplished by removing the `id` attribute of the matching element, + # creating a temporary div at the top of the current viewport, setting the + # hash, and then removing the div and restoring the `id` attribute. + # + # See http://stackoverflow.com/a/1489802/223897 + # + # FIXME (rspeicher): This is still super buggy for me. + setHashWithoutScroll: (hash) -> + @_hash = hash + + # Extract the first ID, in case we were given a range + firstID = hash.replace(/-\d+$/, '') + + $node = $(firstID) + $node.removeAttr('id') + + $tmp = $('
') + .css( + position: 'absolute' + top: "#{$(window).scrollTop()}px" + visibility: 'hidden' + ) + .attr('id', firstID) + .appendTo($('body')) + + @__setLocationHash__(hash) + + $tmp.remove() + $node.attr('id', firstID) + + # Make the actual `location.hash` change + # + # This method is stubbed in tests. + __setLocationHash__: (value) -> + location.hash = value diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 86921f0a777..ab70f4770bc 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -4,8 +4,8 @@ - blob.data.lines.to_a.size.times do |index| - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset - / We're not using `link_to` because it is too slow once we get to thousands of lines. - %a{href: "#L#{i}", id: "L#{i}", rel: "#L#{i}"} + -# We're not using `link_to` because it is too slow once we get to thousands of lines. + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} %i.fa.fa-link = i :preserve diff --git a/spec/javascripts/blob/blob_spec.js.coffee b/spec/javascripts/blob/blob_spec.js.coffee new file mode 100644 index 00000000000..a6f68a53f99 --- /dev/null +++ b/spec/javascripts/blob/blob_spec.js.coffee @@ -0,0 +1,169 @@ +#= require blob/blob + +describe 'BlobView', -> + fixture.preload('blob.html') + + clickLine = (number, eventData = {}) -> + if $.isEmptyObject(eventData) + $("#L#{number}").mousedown().click() + else + e = $.Event 'mousedown', eventData + $("#L#{number}").trigger(e).click() + + beforeEach -> + fixture.load('blob.html') + @class = new BlobView() + @spies = { + __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> + } + + describe 'behavior', -> + it 'highlights one line given in the URL hash', -> + new BlobView('#L13') + expect($('#LC13')).toHaveClass('hll') + + it 'highlights a range of lines given in the URL hash', -> + new BlobView('#L5-25') + expect($('.hll').length).toBe(21) + expect($("#LC#{line}")).toHaveClass('hll') for line in [5..25] + + it 'scrolls to the first highlighted line on initial load', -> + spy = spyOn($, 'scrollTo') + new BlobView('#L5-25') + expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()) + + it 'discards click events', -> + spy = spyOnEvent('a[data-line-number]', 'click') + clickLine(13) + expect(spy).toHaveBeenPrevented() + + it 'handles garbage input from the hash', -> + func = -> new BlobView('#tree-content-holder') + expect(func).not.toThrow() + + describe '#clickHandler', -> + it 'discards the mousedown event', -> + spy = spyOnEvent('a[data-line-number]', 'mousedown') + clickLine(13) + expect(spy).toHaveBeenPrevented() + + describe 'without shiftKey', -> + it 'highlights one line when clicked', -> + clickLine(13) + expect($('#LC13')).toHaveClass('hll') + + it 'unhighlights previously highlighted lines', -> + clickLine(13) + clickLine(20) + + expect($('#LC13')).not.toHaveClass('hll') + expect($('#LC20')).toHaveClass('hll') + + it 'sets the hash', -> + spy = spyOn(@class, 'setHash').and.callThrough() + clickLine(13) + expect(spy).toHaveBeenCalledWith(13) + + describe 'with shiftKey', -> + it 'sets the hash', -> + spy = spyOn(@class, 'setHash').and.callThrough() + clickLine(13) + clickLine(20, shiftKey: true) + expect(spy).toHaveBeenCalledWith(13) + expect(spy).toHaveBeenCalledWith(13, 20) + + describe 'without existing highlight', -> + it 'highlights the clicked line', -> + clickLine(13, shiftKey: true) + expect($('#LC13')).toHaveClass('hll') + expect($('.hll').length).toBe(1) + + it 'sets the hash', -> + spy = spyOn(@class, 'setHash') + clickLine(13, shiftKey: true) + expect(spy).toHaveBeenCalledWith(13) + + describe 'with existing single-line highlight', -> + it 'uses existing line as last line when target is lesser', -> + clickLine(20) + clickLine(15, shiftKey: true) + expect($('.hll').length).toBe(6) + expect($("#LC#{line}")).toHaveClass('hll') for line in [15..20] + + it 'uses existing line as first line when target is greater', -> + clickLine(5) + clickLine(10, shiftKey: true) + expect($('.hll').length).toBe(6) + expect($("#LC#{line}")).toHaveClass('hll') for line in [5..10] + + describe 'with existing multi-line highlight', -> + beforeEach -> + clickLine(10, shiftKey: true) + clickLine(13, shiftKey: true) + + it 'uses target as first line when it is less than existing first line', -> + clickLine(5, shiftKey: true) + expect($('.hll').length).toBe(6) + expect($("#LC#{line}")).toHaveClass('hll') for line in [5..10] + + it 'uses target as last line when it is greater than existing first line', -> + clickLine(15, shiftKey: true) + expect($('.hll').length).toBe(6) + expect($("#LC#{line}")).toHaveClass('hll') for line in [10..15] + + describe '#hashToRange', -> + beforeEach -> + @subject = @class.hashToRange + + it 'extracts a single line number from the hash', -> + expect(@subject('#L5')).toEqual([5, NaN]) + + it 'extracts a range of line numbers from the hash', -> + expect(@subject('#L5-15')).toEqual([5, 15]) + + it 'returns [NaN, NaN] when the hash is not a line number', -> + expect(@subject('#foo')).toEqual([NaN, NaN]) + + describe '#highlightLine', -> + beforeEach -> + @subject = @class.highlightLine + + it 'highlights the specified line', -> + @subject(13) + expect($('#LC13')).toHaveClass('hll') + + it 'accepts a String-based number', -> + @subject('13') + expect($('#LC13')).toHaveClass('hll') + + it 'returns undefined when given NaN', -> + expect(@subject(NaN)).toBe(undefined) + expect(@subject('foo')).toBe(undefined) + + describe '#highlightRange', -> + beforeEach -> + @subject = @class.highlightRange + + it 'returns undefined when first line is NaN', -> + expect(@subject([NaN, 15])).toBe(undefined) + expect(@subject(['foo', 15])).toBe(undefined) + + it 'returns undefined when given an invalid first line', -> + expect(@subject(['foo', 15])).toBe(undefined) + expect(@subject([NaN, NaN])).toBe(undefined) + expect(@subject('foo')).toBe(undefined) + + describe '#setHash', -> + beforeEach -> + @subject = @class.setHash + + it 'returns undefined when given an invalid first line', -> + expect(@subject('foo', 15)).toBe(undefined) + + it 'sets the location hash for a single line', -> + @subject(5) + expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') + + it 'sets the location hash for a range', -> + @subject(5, 15) + expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15') diff --git a/spec/javascripts/fixtures/blob.html.haml b/spec/javascripts/fixtures/blob.html.haml new file mode 100644 index 00000000000..15ad1d8968f --- /dev/null +++ b/spec/javascripts/fixtures/blob.html.haml @@ -0,0 +1,9 @@ +#tree-content-holder + .file-content + .line-numbers + - 1.upto(25) do |i| + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}= i + %pre.code.highlight + %code + - 1.upto(25) do |i| + %span.line{id: "LC#{i}"}= "Line #{i}" -- cgit v1.2.1 From 15582293b9e602f5352a6fe88afd9934c9447dad Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 15 Jun 2015 17:31:41 -0400 Subject: Use `pushState` instead of the temporary div hack --- app/assets/javascripts/blob/blob.js.coffee | 46 +++++++----------------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/blob/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee index b7caae23f31..6df5e870d85 100644 --- a/app/assets/javascripts/blob/blob.js.coffee +++ b/app/assets/javascripts/blob/blob.js.coffee @@ -64,12 +64,11 @@ class @BlobView clickHandler: (event) => event.preventDefault() + @clearHighlight() + lineNumber = $(event.target).data('line-number') current = @hashToRange(@_hash) - # Unhighlight previously highlighted lines - $('.hll').removeClass('hll') - if isNaN(current[0]) or !event.shiftKey # If there's no current selection, or there is but Shift wasn't held, # treat this like a single-line selection. @@ -84,6 +83,10 @@ class @BlobView @setHash(range[0], range[1]) @highlightRange(range) + # Unhighlight previously highlighted lines + clearHighlight: -> + $('.hll').removeClass('hll') + # Convert a URL hash String into line numbers # # hash - Hash String @@ -145,42 +148,13 @@ class @BlobView else hash = "#L#{firstLineNumber}-#{lastLineNumber}" - @setHashWithoutScroll(hash) - - # Prevents the page from scrolling when `location.hash` is set - # - # This is accomplished by removing the `id` attribute of the matching element, - # creating a temporary div at the top of the current viewport, setting the - # hash, and then removing the div and restoring the `id` attribute. - # - # See http://stackoverflow.com/a/1489802/223897 - # - # FIXME (rspeicher): This is still super buggy for me. - setHashWithoutScroll: (hash) -> @_hash = hash - - # Extract the first ID, in case we were given a range - firstID = hash.replace(/-\d+$/, '') - - $node = $(firstID) - $node.removeAttr('id') - - $tmp = $('
') - .css( - position: 'absolute' - top: "#{$(window).scrollTop()}px" - visibility: 'hidden' - ) - .attr('id', firstID) - .appendTo($('body')) - @__setLocationHash__(hash) - $tmp.remove() - $node.attr('id', firstID) - - # Make the actual `location.hash` change + # Make the actual hash change in the browser # # This method is stubbed in tests. __setLocationHash__: (value) -> - location.hash = value + # We're using pushState instead of assigning location.hash directly to + # prevent the page from scrolling on the hashchange event + history.pushState({turbolinks: false, url: value}, document.title, value) -- cgit v1.2.1 From da15428340e94e1c99c60e6e6f794d4c25d8e0a2 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 15 Jun 2015 17:32:37 -0400 Subject: Simplify line numbering --- app/views/shared/_file_highlight.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index ab70f4770bc..d6a2e177da1 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,7 +1,7 @@ .file-content.code{class: user_color_scheme_class} .line-numbers - if blob.data.present? - - blob.data.lines.to_a.size.times do |index| + - blob.data.lines.each_index do |index| - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset -# We're not using `link_to` because it is too slow once we get to thousands of lines. -- cgit v1.2.1 From 1f88d9b56f02ab05aa1ea055a53627b4c934cf51 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 15 Jun 2015 17:33:19 -0400 Subject: Remove "Multiselect Blob" feature specs These are well covered by the new Jasmine tests, and faster! --- features/project/source/multiselect_blob.feature | 85 ----------------------- features/steps/project/source/multiselect_blob.rb | 58 ---------------- 2 files changed, 143 deletions(-) delete mode 100644 features/project/source/multiselect_blob.feature delete mode 100644 features/steps/project/source/multiselect_blob.rb diff --git a/features/project/source/multiselect_blob.feature b/features/project/source/multiselect_blob.feature deleted file mode 100644 index 63b7cb77a93..00000000000 --- a/features/project/source/multiselect_blob.feature +++ /dev/null @@ -1,85 +0,0 @@ -Feature: Project Source Multiselect Blob - Background: - Given I sign in as a user - And I own project "Shop" - And I visit ".gitignore" file in repo - - @javascript - Scenario: I click line 1 in file - When I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I shift-click line 1 in file - When I shift-click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I click line 1 then click line 2 in file - When I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I click line 2 in file - Then I should see "L2" as URI fragment - And I should see line 2 highlighted - - @javascript - Scenario: I click various line numbers to test multiselect - Then I click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I shift-click line 2 in file - Then I should see "L1-2" as URI fragment - And I should see lines 1-2 highlighted - Then I shift-click line 3 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I click line 3 in file - Then I should see "L3" as URI fragment - And I should see line 3 highlighted - Then I shift-click line 1 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I shift-click line 5 in file - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted - Then I shift-click line 4 in file - Then I should see "L1-4" as URI fragment - And I should see lines 1-4 highlighted - Then I click line 5 in file - Then I should see "L5" as URI fragment - And I should see line 5 highlighted - Then I shift-click line 3 in file - Then I should see "L3-5" as URI fragment - And I should see lines 3-5 highlighted - Then I shift-click line 1 in file - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I shift-click line 1 in file - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - - @javascript - Scenario: I multiselect lines 1-5 and then go back and forward in history - When I click line 1 in file - And I shift-click line 3 in file - And I shift-click line 2 in file - And I shift-click line 5 in file - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted - Then I go back in history - Then I should see "L1-2" as URI fragment - And I should see lines 1-2 highlighted - Then I go back in history - Then I should see "L1-3" as URI fragment - And I should see lines 1-3 highlighted - Then I go back in history - Then I should see "L1" as URI fragment - And I should see line 1 highlighted - Then I go forward in history - And I go forward in history - And I go forward in history - Then I should see "L1-5" as URI fragment - And I should see lines 1-5 highlighted diff --git a/features/steps/project/source/multiselect_blob.rb b/features/steps/project/source/multiselect_blob.rb deleted file mode 100644 index 8e14623b892..00000000000 --- a/features/steps/project/source/multiselect_blob.rb +++ /dev/null @@ -1,58 +0,0 @@ -class Spinach::Features::ProjectSourceMultiselectBlob < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - - class << self - def click_line_steps(*line_numbers) - line_numbers.each do |line_number| - step "I click line #{line_number} in file" do - find("#L#{line_number}").click - end - - step "I shift-click line #{line_number} in file" do - script = "$('#L#{line_number}').trigger($.Event('click', { shiftKey: true }));" - execute_script(script) - end - end - end - - def check_state_steps(*ranges) - ranges.each do |range| - fragment = range.kind_of?(Array) ? "L#{range.first}-#{range.last}" : "L#{range}" - pluralization = range.kind_of?(Array) ? "s" : "" - - step "I should see \"#{fragment}\" as URI fragment" do - expect(URI.parse(current_url).fragment).to eq fragment - end - - step "I should see line#{pluralization} #{fragment[1..-1]} highlighted" do - ids = Array(range).map { |n| "LC#{n}" } - extra = false - - highlighted = page.all("#tree-content-holder .highlight .line.hll") - highlighted.each do |element| - extra ||= ids.delete(element[:id]).nil? - end - - expect(extra).to be_false and ids.should be_empty - end - end - end - end - - click_line_steps *Array(1..5) - check_state_steps *Array(1..5), Array(1..2), Array(1..3), Array(1..4), Array(1..5), Array(3..5) - - step 'I go back in history' do - go_back - end - - step 'I go forward in history' do - go_forward - end - - step 'I click on ".gitignore" file in repo' do - click_link ".gitignore" - end -end -- cgit v1.2.1 From 32366d18118281b32b5e770824d637a01d15093b Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 15 Jun 2015 17:53:39 -0400 Subject: Rename BlobView to LineHighlighter --- app/assets/javascripts/blob/blob.js.coffee | 160 ------------------- app/assets/javascripts/dispatcher.js.coffee | 2 +- app/assets/javascripts/line_highlighter.js.coffee | 160 +++++++++++++++++++ spec/javascripts/blob/blob_spec.js.coffee | 169 --------------------- spec/javascripts/fixtures/blob.html.haml | 9 -- .../fixtures/line_highlighter.html.haml | 9 ++ spec/javascripts/line_highlighter_spec.js.coffee | 169 +++++++++++++++++++++ 7 files changed, 339 insertions(+), 339 deletions(-) delete mode 100644 app/assets/javascripts/blob/blob.js.coffee create mode 100644 app/assets/javascripts/line_highlighter.js.coffee delete mode 100644 spec/javascripts/blob/blob_spec.js.coffee delete mode 100644 spec/javascripts/fixtures/blob.html.haml create mode 100644 spec/javascripts/fixtures/line_highlighter.html.haml create mode 100644 spec/javascripts/line_highlighter_spec.js.coffee diff --git a/app/assets/javascripts/blob/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee deleted file mode 100644 index 6df5e870d85..00000000000 --- a/app/assets/javascripts/blob/blob.js.coffee +++ /dev/null @@ -1,160 +0,0 @@ -# BlobView -# -# Handles single- and multi-line selection and highlight for blob views. -# -#= require jquery.scrollTo -# -# ### Example Markup -# -#
-#
-#
-# 1 -# 2 -# 3 -# 4 -# 5 -#
-#
-#         
-#           ...
-#           ...
-#           ...
-#           ...
-#           ...
-#         
-#       
-#
-#
-class @BlobView - # Internal copy of location.hash so we're not dependent on `location` in tests - @_hash = '' - - # Initialize a BlobView object - # - # hash - String URL hash for dependency injection in tests - constructor: (hash = location.hash) -> - @_hash = hash - - @bindEvents() - - unless hash == '' - range = @hashToRange(hash) - - unless isNaN(range[0]) - @highlightRange(range) - - # Scroll to the first highlighted line on initial load - # Offset -50 for the sticky top bar, and another -100 for some context - $.scrollTo("#L#{range[0]}", offset: -150) - - bindEvents: -> - $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler - - # While it may seem odd to bind to the mousedown event and then throw away - # the click event, there is a method to our madness. - # - # If not done this way, the line number anchor will sometimes keep its - # active state even when the event is cancelled, resulting in an ugly border - # around the link and/or a persisted underline text decoration. - - $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) -> - event.preventDefault() - - clickHandler: (event) => - event.preventDefault() - - @clearHighlight() - - lineNumber = $(event.target).data('line-number') - current = @hashToRange(@_hash) - - if isNaN(current[0]) or !event.shiftKey - # If there's no current selection, or there is but Shift wasn't held, - # treat this like a single-line selection. - @setHash(lineNumber) - @highlightLine(lineNumber) - else if event.shiftKey - if lineNumber < current[0] - range = [lineNumber, current[0]] - else - range = [current[0], lineNumber] - - @setHash(range[0], range[1]) - @highlightRange(range) - - # Unhighlight previously highlighted lines - clearHighlight: -> - $('.hll').removeClass('hll') - - # Convert a URL hash String into line numbers - # - # hash - Hash String - # - # Examples: - # - # hashToRange('#L5') # => [5, NaN] - # hashToRange('#L5-15') # => [5, 15] - # hashToRange('#foo') # => [NaN, NaN] - # - # Returns an Array - hashToRange: (hash) -> - first = parseInt(hash.replace(/^#L(\d+)/, '$1')) - last = parseInt(hash.replace(/^#L\d+-(\d+)/, '$1')) - - [first, last] - - # Highlight a single line - # - # lineNumber - Number to highlight. Must be parsable as an Integer. - # - # Returns undefined if lineNumber is not parsable as an Integer. - highlightLine: (lineNumber) -> - return if isNaN(parseInt(lineNumber)) - - $("#LC#{lineNumber}").addClass('hll') - - # Highlight all lines within a range - # - # range - An Array of starting and ending line numbers. - # - # Examples: - # - # # Highlight lines 5 through 15 - # highlightRange([5, 15]) - # - # # The first value is required, and must be a number - # highlightRange(['foo', 15]) # Invalid, returns undefined - # highlightRange([NaN, NaN]) # Invalid, returns undefined - # - # # The second value is optional; if omitted, only highlights the first line - # highlightRange([5, NaN]) # Valid - # - # Returns undefined if the first line is NaN. - highlightRange: (range) -> - return if isNaN(range[0]) - - if isNaN(range[1]) - @highlightLine(range[0]) - else - for lineNumber in [range[0]..range[1]] - @highlightLine(lineNumber) - - setHash: (firstLineNumber, lastLineNumber) => - return if isNaN(parseInt(firstLineNumber)) - - if isNaN(parseInt(lastLineNumber)) - hash = "#L#{firstLineNumber}" - else - hash = "#L#{firstLineNumber}-#{lastLineNumber}" - - @_hash = hash - @__setLocationHash__(hash) - - # Make the actual hash change in the browser - # - # This method is stubbed in tests. - __setLocationHash__: (value) -> - # We're using pushState instead of assigning location.hash directly to - # prevent the page from scrolling on the hashchange event - history.pushState({turbolinks: false, url: value}, document.title, value) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index b7ebe6a5c89..84873e389ea 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -87,7 +87,7 @@ class Dispatcher new TreeView() shortcut_handler = new ShortcutsNavigation() when 'projects:blob:show' - new BlobView() + new LineHighlighter() shortcut_handler = new ShortcutsNavigation() when 'projects:labels:new', 'projects:labels:edit' new Labels() diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee new file mode 100644 index 00000000000..a60a04783ac --- /dev/null +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -0,0 +1,160 @@ +# LineHighlighter +# +# Handles single- and multi-line selection and highlight for blob views. +# +#= require jquery.scrollTo +# +# ### Example Markup +# +#
+#
+#
+# 1 +# 2 +# 3 +# 4 +# 5 +#
+#
+#         
+#           ...
+#           ...
+#           ...
+#           ...
+#           ...
+#         
+#       
+#
+#
+class @LineHighlighter + # Internal copy of location.hash so we're not dependent on `location` in tests + @_hash = '' + + # Initialize a LineHighlighter object + # + # hash - String URL hash for dependency injection in tests + constructor: (hash = location.hash) -> + @_hash = hash + + @bindEvents() + + unless hash == '' + range = @hashToRange(hash) + + unless isNaN(range[0]) + @highlightRange(range) + + # Scroll to the first highlighted line on initial load + # Offset -50 for the sticky top bar, and another -100 for some context + $.scrollTo("#L#{range[0]}", offset: -150) + + bindEvents: -> + $('#tree-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler + + # While it may seem odd to bind to the mousedown event and then throw away + # the click event, there is a method to our madness. + # + # If not done this way, the line number anchor will sometimes keep its + # active state even when the event is cancelled, resulting in an ugly border + # around the link and/or a persisted underline text decoration. + + $('#tree-content-holder').on 'click', 'a[data-line-number]', (event) -> + event.preventDefault() + + clickHandler: (event) => + event.preventDefault() + + @clearHighlight() + + lineNumber = $(event.target).data('line-number') + current = @hashToRange(@_hash) + + if isNaN(current[0]) or !event.shiftKey + # If there's no current selection, or there is but Shift wasn't held, + # treat this like a single-line selection. + @setHash(lineNumber) + @highlightLine(lineNumber) + else if event.shiftKey + if lineNumber < current[0] + range = [lineNumber, current[0]] + else + range = [current[0], lineNumber] + + @setHash(range[0], range[1]) + @highlightRange(range) + + # Unhighlight previously highlighted lines + clearHighlight: -> + $('.hll').removeClass('hll') + + # Convert a URL hash String into line numbers + # + # hash - Hash String + # + # Examples: + # + # hashToRange('#L5') # => [5, NaN] + # hashToRange('#L5-15') # => [5, 15] + # hashToRange('#foo') # => [NaN, NaN] + # + # Returns an Array + hashToRange: (hash) -> + first = parseInt(hash.replace(/^#L(\d+)/, '$1')) + last = parseInt(hash.replace(/^#L\d+-(\d+)/, '$1')) + + [first, last] + + # Highlight a single line + # + # lineNumber - Number to highlight. Must be parsable as an Integer. + # + # Returns undefined if lineNumber is not parsable as an Integer. + highlightLine: (lineNumber) -> + return if isNaN(parseInt(lineNumber)) + + $("#LC#{lineNumber}").addClass('hll') + + # Highlight all lines within a range + # + # range - An Array of starting and ending line numbers. + # + # Examples: + # + # # Highlight lines 5 through 15 + # highlightRange([5, 15]) + # + # # The first value is required, and must be a number + # highlightRange(['foo', 15]) # Invalid, returns undefined + # highlightRange([NaN, NaN]) # Invalid, returns undefined + # + # # The second value is optional; if omitted, only highlights the first line + # highlightRange([5, NaN]) # Valid + # + # Returns undefined if the first line is NaN. + highlightRange: (range) -> + return if isNaN(range[0]) + + if isNaN(range[1]) + @highlightLine(range[0]) + else + for lineNumber in [range[0]..range[1]] + @highlightLine(lineNumber) + + setHash: (firstLineNumber, lastLineNumber) => + return if isNaN(parseInt(firstLineNumber)) + + if isNaN(parseInt(lastLineNumber)) + hash = "#L#{firstLineNumber}" + else + hash = "#L#{firstLineNumber}-#{lastLineNumber}" + + @_hash = hash + @__setLocationHash__(hash) + + # Make the actual hash change in the browser + # + # This method is stubbed in tests. + __setLocationHash__: (value) -> + # We're using pushState instead of assigning location.hash directly to + # prevent the page from scrolling on the hashchange event + history.pushState({turbolinks: false, url: value}, document.title, value) diff --git a/spec/javascripts/blob/blob_spec.js.coffee b/spec/javascripts/blob/blob_spec.js.coffee deleted file mode 100644 index a6f68a53f99..00000000000 --- a/spec/javascripts/blob/blob_spec.js.coffee +++ /dev/null @@ -1,169 +0,0 @@ -#= require blob/blob - -describe 'BlobView', -> - fixture.preload('blob.html') - - clickLine = (number, eventData = {}) -> - if $.isEmptyObject(eventData) - $("#L#{number}").mousedown().click() - else - e = $.Event 'mousedown', eventData - $("#L#{number}").trigger(e).click() - - beforeEach -> - fixture.load('blob.html') - @class = new BlobView() - @spies = { - __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> - } - - describe 'behavior', -> - it 'highlights one line given in the URL hash', -> - new BlobView('#L13') - expect($('#LC13')).toHaveClass('hll') - - it 'highlights a range of lines given in the URL hash', -> - new BlobView('#L5-25') - expect($('.hll').length).toBe(21) - expect($("#LC#{line}")).toHaveClass('hll') for line in [5..25] - - it 'scrolls to the first highlighted line on initial load', -> - spy = spyOn($, 'scrollTo') - new BlobView('#L5-25') - expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()) - - it 'discards click events', -> - spy = spyOnEvent('a[data-line-number]', 'click') - clickLine(13) - expect(spy).toHaveBeenPrevented() - - it 'handles garbage input from the hash', -> - func = -> new BlobView('#tree-content-holder') - expect(func).not.toThrow() - - describe '#clickHandler', -> - it 'discards the mousedown event', -> - spy = spyOnEvent('a[data-line-number]', 'mousedown') - clickLine(13) - expect(spy).toHaveBeenPrevented() - - describe 'without shiftKey', -> - it 'highlights one line when clicked', -> - clickLine(13) - expect($('#LC13')).toHaveClass('hll') - - it 'unhighlights previously highlighted lines', -> - clickLine(13) - clickLine(20) - - expect($('#LC13')).not.toHaveClass('hll') - expect($('#LC20')).toHaveClass('hll') - - it 'sets the hash', -> - spy = spyOn(@class, 'setHash').and.callThrough() - clickLine(13) - expect(spy).toHaveBeenCalledWith(13) - - describe 'with shiftKey', -> - it 'sets the hash', -> - spy = spyOn(@class, 'setHash').and.callThrough() - clickLine(13) - clickLine(20, shiftKey: true) - expect(spy).toHaveBeenCalledWith(13) - expect(spy).toHaveBeenCalledWith(13, 20) - - describe 'without existing highlight', -> - it 'highlights the clicked line', -> - clickLine(13, shiftKey: true) - expect($('#LC13')).toHaveClass('hll') - expect($('.hll').length).toBe(1) - - it 'sets the hash', -> - spy = spyOn(@class, 'setHash') - clickLine(13, shiftKey: true) - expect(spy).toHaveBeenCalledWith(13) - - describe 'with existing single-line highlight', -> - it 'uses existing line as last line when target is lesser', -> - clickLine(20) - clickLine(15, shiftKey: true) - expect($('.hll').length).toBe(6) - expect($("#LC#{line}")).toHaveClass('hll') for line in [15..20] - - it 'uses existing line as first line when target is greater', -> - clickLine(5) - clickLine(10, shiftKey: true) - expect($('.hll').length).toBe(6) - expect($("#LC#{line}")).toHaveClass('hll') for line in [5..10] - - describe 'with existing multi-line highlight', -> - beforeEach -> - clickLine(10, shiftKey: true) - clickLine(13, shiftKey: true) - - it 'uses target as first line when it is less than existing first line', -> - clickLine(5, shiftKey: true) - expect($('.hll').length).toBe(6) - expect($("#LC#{line}")).toHaveClass('hll') for line in [5..10] - - it 'uses target as last line when it is greater than existing first line', -> - clickLine(15, shiftKey: true) - expect($('.hll').length).toBe(6) - expect($("#LC#{line}")).toHaveClass('hll') for line in [10..15] - - describe '#hashToRange', -> - beforeEach -> - @subject = @class.hashToRange - - it 'extracts a single line number from the hash', -> - expect(@subject('#L5')).toEqual([5, NaN]) - - it 'extracts a range of line numbers from the hash', -> - expect(@subject('#L5-15')).toEqual([5, 15]) - - it 'returns [NaN, NaN] when the hash is not a line number', -> - expect(@subject('#foo')).toEqual([NaN, NaN]) - - describe '#highlightLine', -> - beforeEach -> - @subject = @class.highlightLine - - it 'highlights the specified line', -> - @subject(13) - expect($('#LC13')).toHaveClass('hll') - - it 'accepts a String-based number', -> - @subject('13') - expect($('#LC13')).toHaveClass('hll') - - it 'returns undefined when given NaN', -> - expect(@subject(NaN)).toBe(undefined) - expect(@subject('foo')).toBe(undefined) - - describe '#highlightRange', -> - beforeEach -> - @subject = @class.highlightRange - - it 'returns undefined when first line is NaN', -> - expect(@subject([NaN, 15])).toBe(undefined) - expect(@subject(['foo', 15])).toBe(undefined) - - it 'returns undefined when given an invalid first line', -> - expect(@subject(['foo', 15])).toBe(undefined) - expect(@subject([NaN, NaN])).toBe(undefined) - expect(@subject('foo')).toBe(undefined) - - describe '#setHash', -> - beforeEach -> - @subject = @class.setHash - - it 'returns undefined when given an invalid first line', -> - expect(@subject('foo', 15)).toBe(undefined) - - it 'sets the location hash for a single line', -> - @subject(5) - expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') - - it 'sets the location hash for a range', -> - @subject(5, 15) - expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15') diff --git a/spec/javascripts/fixtures/blob.html.haml b/spec/javascripts/fixtures/blob.html.haml deleted file mode 100644 index 15ad1d8968f..00000000000 --- a/spec/javascripts/fixtures/blob.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -#tree-content-holder - .file-content - .line-numbers - - 1.upto(25) do |i| - %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}= i - %pre.code.highlight - %code - - 1.upto(25) do |i| - %span.line{id: "LC#{i}"}= "Line #{i}" diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml new file mode 100644 index 00000000000..15ad1d8968f --- /dev/null +++ b/spec/javascripts/fixtures/line_highlighter.html.haml @@ -0,0 +1,9 @@ +#tree-content-holder + .file-content + .line-numbers + - 1.upto(25) do |i| + %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}= i + %pre.code.highlight + %code + - 1.upto(25) do |i| + %span.line{id: "LC#{i}"}= "Line #{i}" diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee new file mode 100644 index 00000000000..d9a1ff2d5bb --- /dev/null +++ b/spec/javascripts/line_highlighter_spec.js.coffee @@ -0,0 +1,169 @@ +#= require line_highlighter + +describe 'LineHighlighter', -> + fixture.preload('line_highlighter.html') + + clickLine = (number, eventData = {}) -> + if $.isEmptyObject(eventData) + $("#L#{number}").mousedown().click() + else + e = $.Event 'mousedown', eventData + $("#L#{number}").trigger(e).click() + + beforeEach -> + fixture.load('line_highlighter.html') + @class = new LineHighlighter() + @spies = { + __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> + } + + describe 'behavior', -> + it 'highlights one line given in the URL hash', -> + new LineHighlighter('#L13') + expect($('#LC13')).toHaveClass('hll') + + it 'highlights a range of lines given in the URL hash', -> + new LineHighlighter('#L5-25') + expect($('.hll').length).toBe(21) + expect($("#LC#{line}")).toHaveClass('hll') for line in [5..25] + + it 'scrolls to the first highlighted line on initial load', -> + spy = spyOn($, 'scrollTo') + new LineHighlighter('#L5-25') + expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()) + + it 'discards click events', -> + spy = spyOnEvent('a[data-line-number]', 'click') + clickLine(13) + expect(spy).toHaveBeenPrevented() + + it 'handles garbage input from the hash', -> + func = -> new LineHighlighter('#tree-content-holder') + expect(func).not.toThrow() + + describe '#clickHandler', -> + it 'discards the mousedown event', -> + spy = spyOnEvent('a[data-line-number]', 'mousedown') + clickLine(13) + expect(spy).toHaveBeenPrevented() + + describe 'without shiftKey', -> + it 'highlights one line when clicked', -> + clickLine(13) + expect($('#LC13')).toHaveClass('hll') + + it 'unhighlights previously highlighted lines', -> + clickLine(13) + clickLine(20) + + expect($('#LC13')).not.toHaveClass('hll') + expect($('#LC20')).toHaveClass('hll') + + it 'sets the hash', -> + spy = spyOn(@class, 'setHash').and.callThrough() + clickLine(13) + expect(spy).toHaveBeenCalledWith(13) + + describe 'with shiftKey', -> + it 'sets the hash', -> + spy = spyOn(@class, 'setHash').and.callThrough() + clickLine(13) + clickLine(20, shiftKey: true) + expect(spy).toHaveBeenCalledWith(13) + expect(spy).toHaveBeenCalledWith(13, 20) + + describe 'without existing highlight', -> + it 'highlights the clicked line', -> + clickLine(13, shiftKey: true) + expect($('#LC13')).toHaveClass('hll') + expect($('.hll').length).toBe(1) + + it 'sets the hash', -> + spy = spyOn(@class, 'setHash') + clickLine(13, shiftKey: true) + expect(spy).toHaveBeenCalledWith(13) + + describe 'with existing single-line highlight', -> + it 'uses existing line as last line when target is lesser', -> + clickLine(20) + clickLine(15, shiftKey: true) + expect($('.hll').length).toBe(6) + expect($("#LC#{line}")).toHaveClass('hll') for line in [15..20] + + it 'uses existing line as first line when target is greater', -> + clickLine(5) + clickLine(10, shiftKey: true) + expect($('.hll').length).toBe(6) + expect($("#LC#{line}")).toHaveClass('hll') for line in [5..10] + + describe 'with existing multi-line highlight', -> + beforeEach -> + clickLine(10, shiftKey: true) + clickLine(13, shiftKey: true) + + it 'uses target as first line when it is less than existing first line', -> + clickLine(5, shiftKey: true) + expect($('.hll').length).toBe(6) + expect($("#LC#{line}")).toHaveClass('hll') for line in [5..10] + + it 'uses target as last line when it is greater than existing first line', -> + clickLine(15, shiftKey: true) + expect($('.hll').length).toBe(6) + expect($("#LC#{line}")).toHaveClass('hll') for line in [10..15] + + describe '#hashToRange', -> + beforeEach -> + @subject = @class.hashToRange + + it 'extracts a single line number from the hash', -> + expect(@subject('#L5')).toEqual([5, NaN]) + + it 'extracts a range of line numbers from the hash', -> + expect(@subject('#L5-15')).toEqual([5, 15]) + + it 'returns [NaN, NaN] when the hash is not a line number', -> + expect(@subject('#foo')).toEqual([NaN, NaN]) + + describe '#highlightLine', -> + beforeEach -> + @subject = @class.highlightLine + + it 'highlights the specified line', -> + @subject(13) + expect($('#LC13')).toHaveClass('hll') + + it 'accepts a String-based number', -> + @subject('13') + expect($('#LC13')).toHaveClass('hll') + + it 'returns undefined when given NaN', -> + expect(@subject(NaN)).toBe(undefined) + expect(@subject('foo')).toBe(undefined) + + describe '#highlightRange', -> + beforeEach -> + @subject = @class.highlightRange + + it 'returns undefined when first line is NaN', -> + expect(@subject([NaN, 15])).toBe(undefined) + expect(@subject(['foo', 15])).toBe(undefined) + + it 'returns undefined when given an invalid first line', -> + expect(@subject(['foo', 15])).toBe(undefined) + expect(@subject([NaN, NaN])).toBe(undefined) + expect(@subject('foo')).toBe(undefined) + + describe '#setHash', -> + beforeEach -> + @subject = @class.setHash + + it 'returns undefined when given an invalid first line', -> + expect(@subject('foo', 15)).toBe(undefined) + + it 'sets the location hash for a single line', -> + @subject(5) + expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') + + it 'sets the location hash for a range', -> + @subject(5, 15) + expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15') -- cgit v1.2.1 From e59aad6e83cbdafcaf100bb86f6fb925f2fb779e Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 19 Jun 2015 02:01:53 -0400 Subject: Refactor LineHighlighter --- app/assets/javascripts/line_highlighter.js.coffee | 70 ++++++++++------------- spec/javascripts/line_highlighter_spec.js.coffee | 63 +++++++------------- 2 files changed, 51 insertions(+), 82 deletions(-) diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee index a60a04783ac..d7de5516421 100644 --- a/app/assets/javascripts/line_highlighter.js.coffee +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -26,9 +26,13 @@ # # # +# class @LineHighlighter + # CSS class applied to highlighted lines + highlightClass: 'hll' + # Internal copy of location.hash so we're not dependent on `location` in tests - @_hash = '' + _hash: '' # Initialize a LineHighlighter object # @@ -41,7 +45,7 @@ class @LineHighlighter unless hash == '' range = @hashToRange(hash) - unless isNaN(range[0]) + if range[0] @highlightRange(range) # Scroll to the first highlighted line on initial load @@ -69,7 +73,7 @@ class @LineHighlighter lineNumber = $(event.target).data('line-number') current = @hashToRange(@_hash) - if isNaN(current[0]) or !event.shiftKey + unless current[0] and event.shiftKey # If there's no current selection, or there is but Shift wasn't held, # treat this like a single-line selection. @setHash(lineNumber) @@ -85,7 +89,7 @@ class @LineHighlighter # Unhighlight previously highlighted lines clearHighlight: -> - $('.hll').removeClass('hll') + $(".#{@highlightClass}").removeClass(@highlightClass) # Convert a URL hash String into line numbers # @@ -93,60 +97,44 @@ class @LineHighlighter # # Examples: # - # hashToRange('#L5') # => [5, NaN] + # hashToRange('#L5') # => [5, null] # hashToRange('#L5-15') # => [5, 15] - # hashToRange('#foo') # => [NaN, NaN] + # hashToRange('#foo') # => [null, null] # # Returns an Array hashToRange: (hash) -> - first = parseInt(hash.replace(/^#L(\d+)/, '$1')) - last = parseInt(hash.replace(/^#L\d+-(\d+)/, '$1')) + matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/) + + if matches and matches.length + first = parseInt(matches[1]) + last = matches[2] and parseInt(matches[2]) or null - [first, last] + [first, last] + else + [null, null] # Highlight a single line # - # lineNumber - Number to highlight. Must be parsable as an Integer. - # - # Returns undefined if lineNumber is not parsable as an Integer. - highlightLine: (lineNumber) -> - return if isNaN(parseInt(lineNumber)) - - $("#LC#{lineNumber}").addClass('hll') + # lineNumber - Line number to highlight + highlightLine: (lineNumber) => + $("#LC#{lineNumber}").addClass(@highlightClass) # Highlight all lines within a range # - # range - An Array of starting and ending line numbers. - # - # Examples: - # - # # Highlight lines 5 through 15 - # highlightRange([5, 15]) - # - # # The first value is required, and must be a number - # highlightRange(['foo', 15]) # Invalid, returns undefined - # highlightRange([NaN, NaN]) # Invalid, returns undefined - # - # # The second value is optional; if omitted, only highlights the first line - # highlightRange([5, NaN]) # Valid - # - # Returns undefined if the first line is NaN. + # range - Array containing the starting and ending line numbers highlightRange: (range) -> - return if isNaN(range[0]) - - if isNaN(range[1]) - @highlightLine(range[0]) - else + if range[1] for lineNumber in [range[0]..range[1]] @highlightLine(lineNumber) + else + @highlightLine(range[0]) + # Set the URL hash string setHash: (firstLineNumber, lastLineNumber) => - return if isNaN(parseInt(firstLineNumber)) - - if isNaN(parseInt(lastLineNumber)) - hash = "#L#{firstLineNumber}" - else + if lastLineNumber hash = "#L#{firstLineNumber}-#{lastLineNumber}" + else + hash = "#L#{firstLineNumber}" @_hash = hash @__setLocationHash__(hash) diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee index d9a1ff2d5bb..14fa487ff7f 100644 --- a/spec/javascripts/line_highlighter_spec.js.coffee +++ b/spec/javascripts/line_highlighter_spec.js.coffee @@ -13,6 +13,7 @@ describe 'LineHighlighter', -> beforeEach -> fixture.load('line_highlighter.html') @class = new LineHighlighter() + @css = @class.highlightClass @spies = { __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> } @@ -20,12 +21,12 @@ describe 'LineHighlighter', -> describe 'behavior', -> it 'highlights one line given in the URL hash', -> new LineHighlighter('#L13') - expect($('#LC13')).toHaveClass('hll') + expect($('#LC13')).toHaveClass(@css) it 'highlights a range of lines given in the URL hash', -> new LineHighlighter('#L5-25') - expect($('.hll').length).toBe(21) - expect($("#LC#{line}")).toHaveClass('hll') for line in [5..25] + expect($(".#{@css}").length).toBe(21) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25] it 'scrolls to the first highlighted line on initial load', -> spy = spyOn($, 'scrollTo') @@ -50,14 +51,14 @@ describe 'LineHighlighter', -> describe 'without shiftKey', -> it 'highlights one line when clicked', -> clickLine(13) - expect($('#LC13')).toHaveClass('hll') + expect($('#LC13')).toHaveClass(@css) it 'unhighlights previously highlighted lines', -> clickLine(13) clickLine(20) - expect($('#LC13')).not.toHaveClass('hll') - expect($('#LC20')).toHaveClass('hll') + expect($('#LC13')).not.toHaveClass(@css) + expect($('#LC20')).toHaveClass(@css) it 'sets the hash', -> spy = spyOn(@class, 'setHash').and.callThrough() @@ -75,8 +76,8 @@ describe 'LineHighlighter', -> describe 'without existing highlight', -> it 'highlights the clicked line', -> clickLine(13, shiftKey: true) - expect($('#LC13')).toHaveClass('hll') - expect($('.hll').length).toBe(1) + expect($('#LC13')).toHaveClass(@css) + expect($(".#{@css}").length).toBe(1) it 'sets the hash', -> spy = spyOn(@class, 'setHash') @@ -87,14 +88,14 @@ describe 'LineHighlighter', -> it 'uses existing line as last line when target is lesser', -> clickLine(20) clickLine(15, shiftKey: true) - expect($('.hll').length).toBe(6) - expect($("#LC#{line}")).toHaveClass('hll') for line in [15..20] + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20] it 'uses existing line as first line when target is greater', -> clickLine(5) clickLine(10, shiftKey: true) - expect($('.hll').length).toBe(6) - expect($("#LC#{line}")).toHaveClass('hll') for line in [5..10] + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] describe 'with existing multi-line highlight', -> beforeEach -> @@ -103,26 +104,26 @@ describe 'LineHighlighter', -> it 'uses target as first line when it is less than existing first line', -> clickLine(5, shiftKey: true) - expect($('.hll').length).toBe(6) - expect($("#LC#{line}")).toHaveClass('hll') for line in [5..10] + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] it 'uses target as last line when it is greater than existing first line', -> clickLine(15, shiftKey: true) - expect($('.hll').length).toBe(6) - expect($("#LC#{line}")).toHaveClass('hll') for line in [10..15] + expect($(".#{@css}").length).toBe(6) + expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15] describe '#hashToRange', -> beforeEach -> @subject = @class.hashToRange it 'extracts a single line number from the hash', -> - expect(@subject('#L5')).toEqual([5, NaN]) + expect(@subject('#L5')).toEqual([5, null]) it 'extracts a range of line numbers from the hash', -> expect(@subject('#L5-15')).toEqual([5, 15]) - it 'returns [NaN, NaN] when the hash is not a line number', -> - expect(@subject('#foo')).toEqual([NaN, NaN]) + it 'returns [null, null] when the hash is not a line number', -> + expect(@subject('#foo')).toEqual([null, null]) describe '#highlightLine', -> beforeEach -> @@ -130,36 +131,16 @@ describe 'LineHighlighter', -> it 'highlights the specified line', -> @subject(13) - expect($('#LC13')).toHaveClass('hll') + expect($('#LC13')).toHaveClass(@css) it 'accepts a String-based number', -> @subject('13') - expect($('#LC13')).toHaveClass('hll') - - it 'returns undefined when given NaN', -> - expect(@subject(NaN)).toBe(undefined) - expect(@subject('foo')).toBe(undefined) - - describe '#highlightRange', -> - beforeEach -> - @subject = @class.highlightRange - - it 'returns undefined when first line is NaN', -> - expect(@subject([NaN, 15])).toBe(undefined) - expect(@subject(['foo', 15])).toBe(undefined) - - it 'returns undefined when given an invalid first line', -> - expect(@subject(['foo', 15])).toBe(undefined) - expect(@subject([NaN, NaN])).toBe(undefined) - expect(@subject('foo')).toBe(undefined) + expect($('#LC13')).toHaveClass(@css) describe '#setHash', -> beforeEach -> @subject = @class.setHash - it 'returns undefined when given an invalid first line', -> - expect(@subject('foo', 15)).toBe(undefined) - it 'sets the location hash for a single line', -> @subject(5) expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') -- cgit v1.2.1 From 9c47d69788f8d4293d6345698d5b161d8f60039a Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 19 Jun 2015 07:19:56 -0700 Subject: Update maintenance documentation to explain no need to recompile asssets for omnibus installations --- CHANGELOG | 1 + doc/raketasks/maintenance.md | 13 +++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 86de9314d80..2d03cd1d763 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.13.0 (unreleased) + - Update maintenance documentation to explain no need to recompile asssets for omnibus installations (Stan Hu) - Support commenting on diffs in side-by-side mode (Stan Hu) - Fix JavaScript error when clicking on the comment button on a diff line that has a comment already (Stan Hu) - Remove project visibility icons from dashboard projects list diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md index 2aca91d5371..69171cd1765 100644 --- a/doc/raketasks/maintenance.md +++ b/doc/raketasks/maintenance.md @@ -165,13 +165,18 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production Sometimes during version upgrades you might end up with some wrong CSS or missing some icons. In that case, try to precompile the assets again. -For Omnibus-packages: -``` -sudo gitlab-rake assets:precompile -``` +Note that this only applies to source installations and does NOT apply to +omnibus packages. For installations from source: ``` cd /home/git/gitlab sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ``` + +For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at +the release of upstream GitLab. The omnibus version includes optimized versions +of those assets. Unless you are modifying the JavaScript / CSS code on your +production machine after installing the package, there should be no reason to redo +rake assets:precompile on the production machine. If you suspect that assets +have been corrupted, you should reinstall the omnibus package. -- cgit v1.2.1 From 39ad10f9fee0ed090e4da2a94d0c9fc8373e0d5a Mon Sep 17 00:00:00 2001 From: Jeff Blaine Date: Thu, 9 Apr 2015 20:36:46 -0400 Subject: Include non-default ssh key location info Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/875 Provide help for people who aren't SSH-clued by indicating how to point to key files that have been named something other than the default. A lot of people seem to assume that the filename they choose is irrelevant and then wonder why their private key is not being found by simple 'ssh' commands. --- doc/ssh/README.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 0acf92fbf54..d6276cb0c4b 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -30,7 +30,7 @@ cat ~/.ssh/id_rsa.pub Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your user profile. Please copy the complete key starting with `ssh-` and ending -with your username and host. +with your username and host. Use code below to copy your public key to the clipboard. Depending on your OS you'll need to use a different command: @@ -77,3 +77,29 @@ information. ### Eclipse How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration + +## Tip: Non-default OpenSSH key file names or locations + +If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). +For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: + +``` +# +# Main gitlab.com server +# +Host gitlab.com +RSAAuthentication yes +IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename +``` + +Another example +``` +# +# Our company's internal Gitlab server +# +Host my-gitlab.company.com +RSAAuthentication yes +IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename +``` + +Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of this topic is beyond the scope of this document. -- cgit v1.2.1 From 06d2c09f95e8c53bdb9ad8b946f918d8a6e55202 Mon Sep 17 00:00:00 2001 From: Jeff Blaine Date: Wed, 15 Apr 2015 09:21:04 -0400 Subject: Show username SSH config example --- doc/ssh/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index d6276cb0c4b..5f44f9351dd 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -80,8 +80,7 @@ How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Ecli ## Tip: Non-default OpenSSH key file names or locations -If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). -For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: +If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following: ``` # @@ -90,6 +89,7 @@ For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza s Host gitlab.com RSAAuthentication yes IdentityFile ~/my-ssh-key-directory/my-gitlab-private-key-filename +User mygitlabusername ``` Another example @@ -102,4 +102,6 @@ RSAAuthentication yes IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename ``` -Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of this topic is beyond the scope of this document. +Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ. + +Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. -- cgit v1.2.1 From 7964e7d6a1c77e470b93ca3cca69b03f506505ac Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 19 Jun 2015 14:59:07 -0400 Subject: Move the User dashboard enum further up in the class --- app/models/user.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 982c05212ce..b075795d40b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -172,6 +172,9 @@ class User < ActiveRecord::Base after_create :post_create_hook after_destroy :post_destroy_hook + # User's Dashboard preference + # Note: When adding an option, it MUST go on the end of the array. + enum dashboard: [:projects, :stars] alias_attribute :private_token, :authentication_token @@ -704,8 +707,4 @@ class User < ActiveRecord::Base def can_be_removed? !solo_owned_groups.present? end - - # User's Dashboard preference - # Note: When adding an option, it MUST go on the end of the array. - enum dashboard: [:projects, :stars] end -- cgit v1.2.1 From 6c8f0fe906d76cf27f22ffcd3475084f6e0398ec Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 19 Jun 2015 14:59:42 -0400 Subject: Add convenience methods to User for getting and setting 2FA status --- app/models/user.rb | 12 ++++++++++++ spec/factories.rb | 2 +- spec/models/user_spec.rb | 24 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index b075795d40b..a2e2d220b3a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -300,6 +300,18 @@ class User < ActiveRecord::Base @reset_token end + # Check if the user has enabled Two-factor Authentication + def two_factor_enabled? + otp_required_for_login + end + + # Set whether or not Two-factor Authentication is enabled for the current user + # + # setting - Boolean + def two_factor_enabled=(setting) + self.otp_required_for_login = setting + end + def namespace_uniq namespace_name = self.username existing_namespace = Namespace.by_path(namespace_name) diff --git a/spec/factories.rb b/spec/factories.rb index 9373a7af024..578a2e4dc69 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -30,7 +30,7 @@ FactoryGirl.define do trait :two_factor do before(:create) do |user| - user.otp_required_for_login = true + user.two_factor_enabled = true user.otp_secret = User.generate_otp_secret(32) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f3e278e5c5f..fa7680fbbec 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -210,6 +210,30 @@ describe User do end end + describe '#two_factor_enabled' do + it 'returns two-factor authentication status' do + enabled = build_stubbed(:user, two_factor_enabled: true) + disabled = build_stubbed(:user) + + expect(enabled).to be_two_factor_enabled + expect(disabled).not_to be_two_factor_enabled + end + end + + describe '#two_factor_enabled=' do + it 'enables two-factor authentication' do + user = build_stubbed(:user, two_factor_enabled: false) + expect { user.two_factor_enabled = true }. + to change { user.two_factor_enabled? }.to(true) + end + + it 'disables two-factor authentication' do + user = build_stubbed(:user, two_factor_enabled: true) + expect { user.two_factor_enabled = false }. + to change { user.two_factor_enabled? }.to(false) + end + end + describe 'authentication token' do it "should have authentication token" do user = create(:user) -- cgit v1.2.1 From 22dd2240a6ec80955b98667c727326135a2f7f53 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 19 Jun 2015 15:04:47 -0400 Subject: Add 2FA status to Admin::Users#show --- app/views/admin/users/show.html.haml | 8 ++++++++ spec/features/admin/admin_users_spec.rb | 28 ++++++++++++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index f7195ac3326..48cd22fc34b 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -50,6 +50,14 @@ = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do %i.fa.fa-times + %li.two-factor-status + %span.light Two-factor Authentication: + %strong{class: @user.two_factor_enabled? ? 'cgreen' : 'cred'} + - if @user.two_factor_enabled? + Enabled + - else + Disabled + %li %span.light Can create groups: %strong diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index f97b69713ce..7f5cb30cb94 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -63,15 +63,35 @@ describe "Admin::Users", feature: true do end describe "GET /admin/users/:id" do - before do + it "should have user info" do visit admin_users_path - click_link "#{@user.name}" - end + click_link @user.name - it "should have user info" do expect(page).to have_content(@user.email) expect(page).to have_content(@user.name) end + + describe 'Two-factor Authentication status' do + it 'shows when enabled' do + @user.update_attribute(:two_factor_enabled, true) + + visit admin_user_path(@user) + + expect_two_factor_status('Enabled') + end + + it 'shows when disabled' do + visit admin_user_path(@user) + + expect_two_factor_status('Disabled') + end + + def expect_two_factor_status(status) + page.within('.two-factor-status') do + expect(page).to have_content(status) + end + end + end end describe "GET /admin/users/:id/edit" do -- cgit v1.2.1 From b6318297fc93ab26108c586af9d34c16fc783589 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 19 Jun 2015 15:14:37 -0400 Subject: Use User#two_factor_enabled instead of otp_required_for_login --- app/controllers/passwords_controller.rb | 2 +- app/controllers/profiles/two_factor_auths_controller.rb | 4 ++-- app/controllers/sessions_controller.rb | 2 +- app/views/profiles/accounts/show.html.haml | 2 +- spec/controllers/profiles/two_factor_auths_controller_spec.rb | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 145f27b67dd..8450ba31021 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -24,7 +24,7 @@ class PasswordsController < Devise::PasswordsController super do |resource| # TODO (rspeicher): In Devise master (> 3.4.1), we can set # `Devise.sign_in_after_reset_password = false` and avoid this mess. - if resource.errors.empty? && resource.try(:otp_required_for_login?) + if resource.errors.empty? && resource.try(:two_factor_enabled?) resource.unlock_access! if unlockable?(resource) # Since we are not signing this user in, we use the :updated_not_active diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index e7579c652fb..03845f1e1ec 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -10,7 +10,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def create if current_user.valid_otp?(params[:pin_code]) - current_user.otp_required_for_login = true + current_user.two_factor_enabled = true @codes = current_user.generate_otp_backup_codes! current_user.save! @@ -30,7 +30,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def destroy current_user.update_attributes({ - otp_required_for_login: false, + two_factor_enabled: false, encrypted_otp_secret: nil, encrypted_otp_secret_iv: nil, encrypted_otp_secret_salt: nil, diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 4d976fe6630..7577fc96d6d 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -57,7 +57,7 @@ class SessionsController < Devise::SessionsController def authenticate_with_two_factor user = self.resource = find_user - return unless user && user.otp_required_for_login + return unless user && user.two_factor_enabled? if user_params[:otp_attempt].present? && session[:otp_user_id] if valid_otp_attempt?(user) diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index ed009c86568..378dfa2dce0 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -36,7 +36,7 @@ .panel-heading Two-factor Authentication .panel-body - - if current_user.otp_required_for_login + - if current_user.two_factor_enabled? .pull-right = link_to 'Disable Two-factor Authentication', profile_two_factor_auth_path, method: :delete, class: 'btn btn-close btn-sm', data: { confirm: 'Are you sure?' } diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb index 65415f21e55..aa09f1a758d 100644 --- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb +++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb @@ -40,11 +40,11 @@ describe Profiles::TwoFactorAuthsController do expect(user).to receive(:valid_otp?).with(pin).and_return(true) end - it 'sets otp_required_for_login' do + it 'sets two_factor_enabled' do go user.reload - expect(user.otp_required_for_login).to eq true + expect(user).to be_two_factor_enabled end it 'presents plaintext codes for the user to save' do @@ -109,13 +109,13 @@ describe Profiles::TwoFactorAuthsController do let!(:codes) { user.generate_otp_backup_codes! } it 'clears all 2FA-related fields' do - expect(user.otp_required_for_login).to eq true + expect(user).to be_two_factor_enabled expect(user.otp_backup_codes).not_to be_nil expect(user.encrypted_otp_secret).not_to be_nil delete :destroy - expect(user.otp_required_for_login).to eq false + expect(user).not_to be_two_factor_enabled expect(user.otp_backup_codes).to be_nil expect(user.encrypted_otp_secret).to be_nil end -- cgit v1.2.1 From 7f5b255c08593200797f5d29793e375e96f320ef Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Fri, 19 Jun 2015 16:43:09 -0400 Subject: Minor style fixes for LineHighlighter --- app/assets/javascripts/line_highlighter.js.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee index d7de5516421..a8b3c1fa33e 100644 --- a/app/assets/javascripts/line_highlighter.js.coffee +++ b/app/assets/javascripts/line_highlighter.js.coffee @@ -73,7 +73,7 @@ class @LineHighlighter lineNumber = $(event.target).data('line-number') current = @hashToRange(@_hash) - unless current[0] and event.shiftKey + unless current[0] && event.shiftKey # If there's no current selection, or there is but Shift wasn't held, # treat this like a single-line selection. @setHash(lineNumber) @@ -105,9 +105,9 @@ class @LineHighlighter hashToRange: (hash) -> matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/) - if matches and matches.length + if matches && matches.length first = parseInt(matches[1]) - last = matches[2] and parseInt(matches[2]) or null + last = if matches[2] then parseInt(matches[2]) else null [first, last] else -- cgit v1.2.1 From c8d4a408f8594c6483c2d1b28706d53e5bd41529 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 18 Jun 2015 21:55:44 -0700 Subject: Disable changing of target branch in new merge request page when a branch has already been specified Closes #1830 --- CHANGELOG | 1 + app/views/projects/_issuable_form.html.haml | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 86de9314d80..c523b23c569 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 7.13.0 (unreleased) - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8 v 7.12.0 (unreleased) + - Disable changing of target branch in new merge request page when a branch has already been specified (Stan Hu) - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) - Update oauth button logos for Twitter and Google to recommended assets - Fix hooks for web based events with external issue references (Daniel Gerhardt) diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml index 4d93c89c93a..496fad34dc2 100644 --- a/app/views/projects/_issuable_form.html.haml +++ b/app/views/projects/_issuable_form.html.haml @@ -15,10 +15,10 @@ - if issuable.is_a?(MergeRequest) %p.help-block - if issuable.work_in_progress? - Remove the WIP prefix from the title to allow this + Remove the WIP prefix from the title to allow this Work In Progress merge request to be accepted when it's ready. - else - Start the title with [WIP] or WIP: to prevent a + Start the title with [WIP] or WIP: to prevent a Work In Progress merge request from being accepted before it's ready. .form-group.issuable-description = f.label :description, 'Description', class: 'control-label' @@ -81,21 +81,22 @@ - if issuable.is_a?(MergeRequest) %hr - - unless @merge_request.persisted? + - if @merge_request.new_record? .form-group = f.label :source_branch, class: 'control-label' do %i.fa.fa-code-fork Source Branch .col-sm-10 = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true }) - %p.help-block - = link_to 'Change source branch', mr_change_branches_path(@merge_request) .form-group = f.label :target_branch, class: 'control-label' do %i.fa.fa-code-fork Target Branch .col-sm-10 - = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2' }) + = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? }) + - if @merge_request.new_record? + %p.help-block + = link_to 'Change branches', mr_change_branches_path(@merge_request) .form-actions - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted? -- cgit v1.2.1 From e785b9d2e24ca7e16e8ff3fa46f2e2b82478be9b Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 19 Jun 2015 21:18:24 -0700 Subject: Fix Error 500 when one user attempts to access a personal, internal snippet Closes #1815 --- CHANGELOG | 1 + app/models/ability.rb | 2 +- features/snippets/snippets.feature | 13 ++++++++++++- features/steps/shared/authentication.rb | 4 ++++ features/steps/snippets/snippets.rb | 20 ++++++++++++++++++++ spec/support/login_helpers.rb | 5 +++++ 6 files changed, 43 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 86de9314d80..26d750311a8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ v 7.13.0 (unreleased) - Update ssl_ciphers in Nginx example to remove DHE settings. This will deny forward secrecy for Android 2.3.7, Java 6 and OpenSSL 0.9.8 v 7.12.0 (unreleased) + - Fix Error 500 when one user attempts to access a personal, internal snippet (Stan Hu) - Fix post-receive errors on a push when an external issue tracker is configured (Stan Hu) - Update oauth button logos for Twitter and Google to recommended assets - Fix hooks for web based events with external issue references (Daniel Gerhardt) diff --git a/app/models/ability.rb b/app/models/ability.rb index bcd2adee00b..a5db22040e0 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -263,7 +263,7 @@ class Ability :"modify_#{name}", ] else - if subject.respond_to?(:project) + if subject.respond_to?(:project) && subject.project project_abilities(user, subject.project) else [] diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature index 6e8019c326f..4f617b6bed8 100644 --- a/features/snippets/snippets.feature +++ b/features/snippets/snippets.feature @@ -25,4 +25,15 @@ Feature: Snippets Scenario: I destroy "Personal snippet one" Given I visit snippet page "Personal snippet one" And I click link "Destroy" - Then I should not see "Personal snippet one" in snippets \ No newline at end of file + Then I should not see "Personal snippet one" in snippets + + Scenario: I create new internal snippet + Given I logout directly + And I sign in as an admin + Then I visit new snippet page + And I submit new internal snippet + Then I visit snippet page "Internal personal snippet one" + And I logout directly + Then I sign in as a user + Given I visit new snippet page + Then I visit snippet page "Internal personal snippet one" diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb index 3c0f2a9406a..735e0ef6108 100644 --- a/features/steps/shared/authentication.rb +++ b/features/steps/shared/authentication.rb @@ -28,6 +28,10 @@ module SharedAuthentication logout end + step "I logout directly" do + logout_direct + end + def current_user @user || User.first end diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index 09fdd1b5a13..426da2918ea 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -31,6 +31,18 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps click_button "Create snippet" end + step 'I submit new internal snippet' do + fill_in "personal_snippet_title", :with => "Internal personal snippet one" + fill_in "personal_snippet_file_name", :with => "my_snippet.rb" + choose 'personal_snippet_visibility_level_10' + + page.within('.file-editor') do + find(:xpath, "//input[@id='personal_snippet_content']").set 'Content of internal snippet' + end + + click_button "Create snippet" + end + step 'I should see snippet "Personal snippet three"' do expect(page).to have_content "Personal snippet three" expect(page).to have_content "Content of snippet three" @@ -58,7 +70,15 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps visit snippet_path(snippet) end + step 'I visit snippet page "Internal personal snippet one"' do + visit snippet_path(internal_snippet) + end + def snippet @snippet ||= PersonalSnippet.find_by!(title: "Personal snippet one") end + + def internal_snippet + @snippet ||= PersonalSnippet.find_by!(title: "Internal personal snippet one") + end end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 1bd68552012..ffe30a4246c 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -39,4 +39,9 @@ module LoginHelpers def logout find(:css, ".fa.fa-sign-out").click end + + # Logout without JavaScript driver + def logout_direct + page.driver.submit :delete, '/users/sign_out', {} + end end -- cgit v1.2.1