Commit b1bd3f1252eb529030f2295e4c2a991158894b64
1 parent
124a5e27
Exists in
master
and in
4 other branches
fix tests. added jquery.timeago.js
Showing
14 changed files
with
247 additions
and
82 deletions
Show diff stats
... | ... | @@ -0,0 +1,181 @@ |
1 | +/** | |
2 | + * Timeago is a jQuery plugin that makes it easy to support automatically | |
3 | + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). | |
4 | + * | |
5 | + * @name timeago | |
6 | + * @version 1.1.0 | |
7 | + * @requires jQuery v1.2.3+ | |
8 | + * @author Ryan McGeary | |
9 | + * @license MIT License - http://www.opensource.org/licenses/mit-license.php | |
10 | + * | |
11 | + * For usage and examples, visit: | |
12 | + * http://timeago.yarp.com/ | |
13 | + * | |
14 | + * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) | |
15 | + */ | |
16 | + | |
17 | +(function (factory) { | |
18 | + if (typeof define === 'function' && define.amd) { | |
19 | + // AMD. Register as an anonymous module. | |
20 | + define(['jquery'], factory); | |
21 | + } else { | |
22 | + // Browser globals | |
23 | + factory(jQuery); | |
24 | + } | |
25 | +}(function ($) { | |
26 | + $.timeago = function(timestamp) { | |
27 | + if (timestamp instanceof Date) { | |
28 | + return inWords(timestamp); | |
29 | + } else if (typeof timestamp === "string") { | |
30 | + return inWords($.timeago.parse(timestamp)); | |
31 | + } else if (typeof timestamp === "number") { | |
32 | + return inWords(new Date(timestamp)); | |
33 | + } else { | |
34 | + return inWords($.timeago.datetime(timestamp)); | |
35 | + } | |
36 | + }; | |
37 | + var $t = $.timeago; | |
38 | + | |
39 | + $.extend($.timeago, { | |
40 | + settings: { | |
41 | + refreshMillis: 60000, | |
42 | + allowFuture: false, | |
43 | + strings: { | |
44 | + prefixAgo: null, | |
45 | + prefixFromNow: null, | |
46 | + suffixAgo: "ago", | |
47 | + suffixFromNow: "from now", | |
48 | + seconds: "less than a minute", | |
49 | + minute: "about a minute", | |
50 | + minutes: "%d minutes", | |
51 | + hour: "about an hour", | |
52 | + hours: "about %d hours", | |
53 | + day: "a day", | |
54 | + days: "%d days", | |
55 | + month: "about a month", | |
56 | + months: "%d months", | |
57 | + year: "about a year", | |
58 | + years: "%d years", | |
59 | + wordSeparator: " ", | |
60 | + numbers: [] | |
61 | + } | |
62 | + }, | |
63 | + inWords: function(distanceMillis) { | |
64 | + var $l = this.settings.strings; | |
65 | + var prefix = $l.prefixAgo; | |
66 | + var suffix = $l.suffixAgo; | |
67 | + if (this.settings.allowFuture) { | |
68 | + if (distanceMillis < 0) { | |
69 | + prefix = $l.prefixFromNow; | |
70 | + suffix = $l.suffixFromNow; | |
71 | + } | |
72 | + } | |
73 | + | |
74 | + var seconds = Math.abs(distanceMillis) / 1000; | |
75 | + var minutes = seconds / 60; | |
76 | + var hours = minutes / 60; | |
77 | + var days = hours / 24; | |
78 | + var years = days / 365; | |
79 | + | |
80 | + function substitute(stringOrFunction, number) { | |
81 | + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; | |
82 | + var value = ($l.numbers && $l.numbers[number]) || number; | |
83 | + return string.replace(/%d/i, value); | |
84 | + } | |
85 | + | |
86 | + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || | |
87 | + seconds < 90 && substitute($l.minute, 1) || | |
88 | + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || | |
89 | + minutes < 90 && substitute($l.hour, 1) || | |
90 | + hours < 24 && substitute($l.hours, Math.round(hours)) || | |
91 | + hours < 42 && substitute($l.day, 1) || | |
92 | + days < 30 && substitute($l.days, Math.round(days)) || | |
93 | + days < 45 && substitute($l.month, 1) || | |
94 | + days < 365 && substitute($l.months, Math.round(days / 30)) || | |
95 | + years < 1.5 && substitute($l.year, 1) || | |
96 | + substitute($l.years, Math.round(years)); | |
97 | + | |
98 | + var separator = $l.wordSeparator || ""; | |
99 | + if ($l.wordSeparator === undefined) { separator = " "; } | |
100 | + return $.trim([prefix, words, suffix].join(separator)); | |
101 | + }, | |
102 | + parse: function(iso8601) { | |
103 | + var s = $.trim(iso8601); | |
104 | + s = s.replace(/\.\d+/,""); // remove milliseconds | |
105 | + s = s.replace(/-/,"/").replace(/-/,"/"); | |
106 | + s = s.replace(/T/," ").replace(/Z/," UTC"); | |
107 | + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 | |
108 | + return new Date(s); | |
109 | + }, | |
110 | + datetime: function(elem) { | |
111 | + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); | |
112 | + return $t.parse(iso8601); | |
113 | + }, | |
114 | + isTime: function(elem) { | |
115 | + // jQuery's `is()` doesn't play well with HTML5 in IE | |
116 | + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); | |
117 | + } | |
118 | + }); | |
119 | + | |
120 | + // functions that can be called via $(el).timeago('action') | |
121 | + // init is default when no action is given | |
122 | + // functions are called with context of a single element | |
123 | + var functions = { | |
124 | + init: function(){ | |
125 | + var refresh_el = $.proxy(refresh, this); | |
126 | + refresh_el(); | |
127 | + var $s = $t.settings; | |
128 | + if ($s.refreshMillis > 0) { | |
129 | + setInterval(refresh_el, $s.refreshMillis); | |
130 | + } | |
131 | + }, | |
132 | + update: function(time){ | |
133 | + $(this).data('timeago', { datetime: $t.parse(time) }); | |
134 | + refresh.apply(this); | |
135 | + } | |
136 | + }; | |
137 | + | |
138 | + $.fn.timeago = function(action, options) { | |
139 | + var fn = action ? functions[action] : functions.init; | |
140 | + if(!fn){ | |
141 | + throw new Error("Unknown function name '"+ action +"' for timeago"); | |
142 | + } | |
143 | + // each over objects here and call the requested function | |
144 | + this.each(function(){ | |
145 | + fn.call(this, options); | |
146 | + }); | |
147 | + return this; | |
148 | + }; | |
149 | + | |
150 | + function refresh() { | |
151 | + var data = prepareData(this); | |
152 | + if (!isNaN(data.datetime)) { | |
153 | + $(this).text(inWords(data.datetime)); | |
154 | + } | |
155 | + return this; | |
156 | + } | |
157 | + | |
158 | + function prepareData(element) { | |
159 | + element = $(element); | |
160 | + if (!element.data("timeago")) { | |
161 | + element.data("timeago", { datetime: $t.datetime(element) }); | |
162 | + var text = $.trim(element.text()); | |
163 | + if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { | |
164 | + element.attr("title", text); | |
165 | + } | |
166 | + } | |
167 | + return element.data("timeago"); | |
168 | + } | |
169 | + | |
170 | + function inWords(date) { | |
171 | + return $t.inWords(distance(date)); | |
172 | + } | |
173 | + | |
174 | + function distance(date) { | |
175 | + return (new Date().getTime() - date.getTime()); | |
176 | + } | |
177 | + | |
178 | + // fix for IE6 suckage | |
179 | + document.createElement("abbr"); | |
180 | + document.createElement("time"); | |
181 | +})); | ... | ... |
app/assets/javascripts/main.js.coffee
app/assets/javascripts/wall.js.coffee
... | ... | @@ -30,24 +30,13 @@ |
30 | 30 | Wall.note_ids.push(note.id) |
31 | 31 | Wall.renderNote(note) |
32 | 32 | Wall.scrollDown() |
33 | + $("abbr.timeago").timeago() | |
33 | 34 | |
34 | 35 | complete: -> |
35 | 36 | $('.js-notes-busy').removeClass("loading") |
36 | 37 | beforeSend: -> |
37 | 38 | $('.js-notes-busy').addClass("loading") |
38 | 39 | |
39 | - renderNote: (note) -> | |
40 | - author = '<strong class="wall-author">' + note.author.name + '</strong>' | |
41 | - body = '<span class="wall-text">' + note.body + '</span>' | |
42 | - file = '' | |
43 | - | |
44 | - if note.attachment | |
45 | - file = '<span class="wall-file"><a href="/files/note/' + note.id + '/' + note.attachment + '">' + note.attachment + '</a></span>' | |
46 | - | |
47 | - html = '<li>' + author + body + file + '</li>' | |
48 | - | |
49 | - $('ul.notes').append(html) | |
50 | - | |
51 | 40 | initRefresh: -> |
52 | 41 | setInterval("Wall.refresh()", 10000) |
53 | 42 | |
... | ... | @@ -59,14 +48,9 @@ |
59 | 48 | $('body').scrollTop(notes.height()) |
60 | 49 | |
61 | 50 | initForm: -> |
62 | - form = $('.new_note') | |
51 | + form = $('.wall-note-form') | |
63 | 52 | form.find("#target_type").val('wall') |
64 | 53 | |
65 | - # remove unnecessary fields and buttons | |
66 | - form.find("#note_line_code").remove() | |
67 | - form.find(".js-close-discussion-note-form").remove() | |
68 | - form.find('.js-notify-commit-author').remove() | |
69 | - | |
70 | 54 | form.on 'ajax:success', -> |
71 | 55 | Wall.refresh() |
72 | 56 | form.find(".js-note-text").val("").trigger("input") |
... | ... | @@ -83,3 +67,17 @@ |
83 | 67 | form.find(".js-attachment-filename").text(filename) |
84 | 68 | |
85 | 69 | form.show() |
70 | + | |
71 | + renderNote: (note) -> | |
72 | + author = '<strong class="wall-author">' + note.author.name + '</strong>' | |
73 | + body = '<span class="wall-text">' + note.body + '</span>' | |
74 | + file = '' | |
75 | + time = '<abbr class="timeago" title="' + note.created_at + '">' + note.created_at + '</time>' | |
76 | + | |
77 | + if note.attachment | |
78 | + file = '<span class="wall-file"><a href="/files/note/' + note.id + '/' + note.attachment + '">' + note.attachment + '</a></span>' | |
79 | + | |
80 | + html = '<li>' + author + body + file + time + '</li>' | |
81 | + | |
82 | + $('ul.notes').append(html) | |
83 | + | ... | ... |
app/assets/stylesheets/sections/wall.scss
1 | 1 | .wall-page { |
2 | - .new_note { | |
2 | + .wall-note-form { | |
3 | 3 | @extend .span12; |
4 | 4 | |
5 | 5 | margin: 0; |
... | ... | @@ -23,7 +23,14 @@ |
23 | 23 | } |
24 | 24 | |
25 | 25 | .wall-file { |
26 | + margin-left: 8px; | |
27 | + background: #EEE; | |
28 | + } | |
29 | + | |
30 | + abbr { | |
26 | 31 | float: right; |
32 | + color: #AAA; | |
33 | + border: none; | |
27 | 34 | } |
28 | 35 | } |
29 | 36 | } | ... | ... |
app/views/events/event/_note.html.haml
app/views/notify/note_wall_email.html.haml
app/views/notify/note_wall_email.text.erb
app/views/walls/show.html.haml
... | ... | @@ -2,9 +2,30 @@ |
2 | 2 | %ul.well-list.notes |
3 | 3 | .notes-busy.js-notes-busy |
4 | 4 | |
5 | - .js-main-target-form | |
6 | 5 | - if can? current_user, :write_note, @project |
7 | - = render "notes/form" | |
6 | + .note-form-holder | |
7 | + = form_for [@project, @note], remote: true, html: { multipart: true, id: nil, class: "new_note wall-note-form" } do |f| | |
8 | + = note_target_fields | |
9 | + .note_text_and_preview | |
10 | + = f.text_area :note, size: 255, class: 'note_text js-note-text js-gfm-input turn-on' | |
11 | + .note-form-actions | |
12 | + .buttons | |
13 | + = f.submit 'Add Comment', class: "btn comment-btn grouped js-comment-button" | |
14 | + | |
15 | + .note-form-option | |
16 | + = label_tag :notify do | |
17 | + = check_box_tag :notify, 1, false | |
18 | + %span.light Notify team via email | |
19 | + | |
20 | + .note-form-option | |
21 | + %a.choose-btn.btn.btn-small.js-choose-note-attachment-button | |
22 | + %i.icon-paper-clip | |
23 | + %span Choose File ... | |
24 | + | |
25 | + %span.file_name.js-attachment-filename File name... | |
26 | + = f.file_field :attachment, class: "js-note-attachment-input hide" | |
27 | + | |
28 | + .clearfix | |
8 | 29 | |
9 | 30 | :javascript |
10 | 31 | $(function(){ | ... | ... |
features/steps/shared/paths.rb
spec/features/gitlab_flavored_markdown_spec.rb
... | ... | @@ -198,7 +198,7 @@ describe "Gitlab Flavored Markdown" do |
198 | 198 | end |
199 | 199 | |
200 | 200 | it "should render in projects#wall", js: true do |
201 | - visit wall_project_path(project) | |
201 | + visit project_wall_path(project) | |
202 | 202 | within ".new_note.js-main-target-form" do |
203 | 203 | fill_in "note_note", with: "see ##{issue.id}" |
204 | 204 | click_button "Add Comment" | ... | ... |
spec/features/notes_on_merge_requests_spec.rb
... | ... | @@ -22,7 +22,7 @@ describe "On a merge request", js: true do |
22 | 22 | it { within(".js-main-target-form") { should_not have_link("Cancel") } } |
23 | 23 | |
24 | 24 | # notifiactions |
25 | - it { within(".js-main-target-form") { should have_checked_field("Notify team via email") } } | |
25 | + it { within(".js-main-target-form") { should have_unchecked_field("Notify team via email") } } | |
26 | 26 | it { within(".js-main-target-form") { should_not have_checked_field("Notify commit author") } } |
27 | 27 | it { within(".js-main-target-form") { should_not have_unchecked_field("Notify commit author") } } |
28 | 28 | |
... | ... | @@ -127,7 +127,7 @@ describe "On a merge request diff", js: true, focus: true do |
127 | 127 | it { should have_css(".js-close-discussion-note-form", text: "Cancel") } |
128 | 128 | |
129 | 129 | # notification options |
130 | - it { should have_checked_field("Notify team via email") } | |
130 | + it { should have_unchecked_field("Notify team via email") } | |
131 | 131 | |
132 | 132 | it "shouldn't add a second form for same row" do |
133 | 133 | find("#4735dfc552ad7bf15ca468adc3cad9d05b624490_185_185.line_holder .js-add-diff-note-button").trigger("click") | ... | ... |
spec/features/notes_on_wall_spec.rb
... | ... | @@ -2,84 +2,40 @@ require 'spec_helper' |
2 | 2 | |
3 | 3 | describe "On the project wall", js: true do |
4 | 4 | let!(:project) { create(:project) } |
5 | - let!(:commit) { project.repository.commit("bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a") } | |
6 | 5 | |
7 | 6 | before do |
8 | 7 | login_as :user |
9 | 8 | project.team << [@user, :master] |
10 | - visit wall_project_path(project) | |
9 | + visit project_wall_path(project) | |
11 | 10 | end |
12 | 11 | |
13 | 12 | subject { page } |
14 | 13 | |
15 | 14 | describe "the note form" do |
16 | - # main target form creation | |
17 | - it { should have_css(".js-main-target-form", visible: true, count: 1) } | |
18 | - | |
19 | - # button initalization | |
20 | - it { find(".js-main-target-form input[type=submit]").value.should == "Add Comment" } | |
21 | - it { within(".js-main-target-form") { should_not have_link("Cancel") } } | |
22 | - | |
23 | - # notifiactions | |
24 | - it { within(".js-main-target-form") { should have_checked_field("Notify team via email") } } | |
25 | - it { within(".js-main-target-form") { should_not have_checked_field("Notify commit author") } } | |
26 | - it { within(".js-main-target-form") { should_not have_unchecked_field("Notify commit author") } } | |
27 | - | |
28 | - describe "without text" do | |
29 | - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } } | |
30 | - end | |
15 | + it { should have_css(".wall-note-form", visible: true, count: 1) } | |
16 | + it { find(".wall-note-form input[type=submit]").value.should == "Add Comment" } | |
17 | + it { within(".wall-note-form") { should have_unchecked_field("Notify team via email") } } | |
31 | 18 | |
32 | 19 | describe "with text" do |
33 | 20 | before do |
34 | - within(".js-main-target-form") do | |
21 | + within(".wall-note-form") do | |
35 | 22 | fill_in "note[note]", with: "This is awesome" |
36 | 23 | end |
37 | 24 | end |
38 | 25 | |
39 | - it { within(".js-main-target-form") { should_not have_css(".js-comment-button[disabled]") } } | |
40 | - | |
41 | - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: true) } } | |
42 | - end | |
43 | - | |
44 | - describe "with preview" do | |
45 | - before do | |
46 | - within(".js-main-target-form") do | |
47 | - fill_in "note[note]", with: "This is awesome" | |
48 | - find(".js-note-preview-button").trigger("click") | |
49 | - end | |
50 | - end | |
51 | - | |
52 | - it { within(".js-main-target-form") { should have_css(".js-note-preview", text: "This is awesome", visible: true) } } | |
53 | - | |
54 | - it { within(".js-main-target-form") { should have_css(".js-note-preview-button", visible: false) } } | |
55 | - it { within(".js-main-target-form") { should have_css(".js-note-edit-button", visible: true) } } | |
26 | + it { within(".wall-note-form") { should_not have_css(".js-comment-button[disabled]") } } | |
56 | 27 | end |
57 | 28 | end |
58 | 29 | |
59 | 30 | describe "when posting a note" do |
60 | 31 | before do |
61 | - within(".js-main-target-form") do | |
32 | + within(".wall-note-form") do | |
62 | 33 | fill_in "note[note]", with: "This is awsome!" |
63 | - find(".js-note-preview-button").trigger("click") | |
64 | 34 | click_button "Add Comment" |
65 | 35 | end |
66 | 36 | end |
67 | 37 | |
68 | - # note added | |
69 | 38 | it { should have_content("This is awsome!") } |
70 | - | |
71 | - # reset form | |
72 | - it { within(".js-main-target-form") { should have_no_field("note[note]", with: "This is awesome!") } } | |
73 | - | |
74 | - # return from preview | |
75 | - it { within(".js-main-target-form") { should have_css(".js-note-preview", visible: false) } } | |
76 | - it { within(".js-main-target-form") { should have_css(".js-note-text", visible: true) } } | |
77 | - | |
78 | - | |
79 | - it "should be removable" do | |
80 | - find(".js-note-delete").trigger("click") | |
81 | - | |
82 | - should_not have_css(".note") | |
83 | - end | |
39 | + it { within(".wall-note-form") { should have_no_field("note[note]", with: "This is awesome!") } } | |
84 | 40 | end |
85 | 41 | end | ... | ... |
spec/features/security/project_access_spec.rb
... | ... | @@ -95,7 +95,7 @@ describe "Application access" do |
95 | 95 | end |
96 | 96 | |
97 | 97 | describe "GET /project_code/wall" do |
98 | - subject { wall_project_path(project) } | |
98 | + subject { project_wall_path(project) } | |
99 | 99 | |
100 | 100 | it { should be_allowed_for master } |
101 | 101 | it { should be_allowed_for reporter } | ... | ... |
spec/mailers/notify_spec.rb
... | ... | @@ -239,7 +239,7 @@ describe Notify do |
239 | 239 | end |
240 | 240 | |
241 | 241 | describe 'on a project wall' do |
242 | - let(:note_on_the_wall_path) { wall_project_path(project, anchor: "note_#{note.id}") } | |
242 | + let(:note_on_the_wall_path) { project_wall_path(project, anchor: "note_#{note.id}") } | |
243 | 243 | |
244 | 244 | subject { Notify.note_wall_email(recipient.id, note.id) } |
245 | 245 | ... | ... |