BigW Consortium Gitlab
Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
G
gitlab-ce
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
Forest Godfrey
gitlab-ce
Commits
cf34b07e
Commit
cf34b07e
authored
May 03, 2017
by
Eric Eastwood
Committed by
Jacob Schatz
May 03, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add real-time note edits
🐿
parent
e14ca539
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
172 additions
and
62 deletions
+172
-62
gl_form.js
app/assets/javascripts/gl_form.js
+2
-2
notes.js
app/assets/javascripts/notes.js
+0
-0
notes.scss
app/assets/stylesheets/pages/notes.scss
+1
-1
_edit_form.html.haml
app/views/projects/notes/_edit_form.html.haml
+1
-1
_note.html.haml
app/views/shared/notes/_note.html.haml
+5
-1
30458-real-time-note-edits.yml
changelogs/unreleased/30458-real-time-note-edits.yml
+4
-0
gitlab_flavored_markdown_spec.rb
spec/features/gitlab_flavored_markdown_spec.rb
+2
-8
note_polling_spec.rb
spec/features/issues/note_polling_spec.rb
+69
-6
notes_spec.js
spec/javascripts/notes_spec.js
+88
-43
No files found.
app/assets/javascripts/gl_form.js
View file @
cf34b07e
...
@@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() {
...
@@ -34,9 +34,9 @@ GLForm.prototype.setupForm = function() {
gl
.
GfmAutoComplete
.
setup
(
this
.
form
.
find
(
'.js-gfm-input'
));
gl
.
GfmAutoComplete
.
setup
(
this
.
form
.
find
(
'.js-gfm-input'
));
new
DropzoneInput
(
this
.
form
);
new
DropzoneInput
(
this
.
form
);
autosize
(
this
.
textarea
);
autosize
(
this
.
textarea
);
// form and textarea event listeners
this
.
addEventListeners
();
}
}
// form and textarea event listeners
this
.
addEventListeners
();
gl
.
text
.
init
(
this
.
form
);
gl
.
text
.
init
(
this
.
form
);
// hide discard button
// hide discard button
this
.
form
.
find
(
'.js-note-discard'
).
hide
();
this
.
form
.
find
(
'.js-note-discard'
).
hide
();
...
...
app/assets/javascripts/notes.js
View file @
cf34b07e
This diff is collapsed.
Click to expand it.
app/assets/stylesheets/pages/notes.scss
View file @
cf34b07e
...
@@ -67,7 +67,7 @@ ul.notes {
...
@@ -67,7 +67,7 @@ ul.notes {
}
}
}
}
&
.is-edit
t
ing
{
&
.is-editing
{
.note-header
,
.note-header
,
.note-text
,
.note-text
,
.edited-text
{
.edited-text
{
...
...
app/views/projects/notes/_edit_form.html.haml
View file @
cf34b07e
...
@@ -7,7 +7,7 @@
...
@@ -7,7 +7,7 @@
=
render
'projects/notes/hints'
=
render
'projects/notes/hints'
.note-form-actions.clearfix
.note-form-actions.clearfix
.settings-message.note-edit-warning.js-edit-warning
.settings-message.note-edit-warning.js-
finish-
edit-warning
Finish editing this message first!
Finish editing this message first!
=
submit_tag
'Save comment'
,
class:
'btn btn-nr btn-save js-comment-button'
=
submit_tag
'Save comment'
,
class:
'btn btn-nr btn-save js-comment-button'
%button
.btn.btn-nr.btn-cancel.note-edit-cancel
{
type:
'button'
}
%button
.btn.btn-nr.btn-cancel.note-edit-cancel
{
type:
'button'
}
...
...
app/views/shared/notes/_note.html.haml
View file @
cf34b07e
...
@@ -2,7 +2,11 @@
...
@@ -2,7 +2,11 @@
-
return
if
note
.
cross_reference_not_visible_for?
(
current_user
)
-
return
if
note
.
cross_reference_not_visible_for?
(
current_user
)
-
note_editable
=
note_editable?
(
note
)
-
note_editable
=
note_editable?
(
note
)
%li
.timeline-entry
{
id:
dom_id
(
note
),
class:
[
"note"
,
"note-row-#{note.id}"
,
(
'system-note'
if
note
.
system
)],
data:
{
author_id:
note
.
author
.
id
,
editable:
note_editable
,
note_id:
note
.
id
}
}
%li
.timeline-entry
{
id:
dom_id
(
note
),
class:
[
"note"
,
"note-row-#{note.id}"
,
(
'system-note'
if
note
.
system
)],
data:
{
author_id:
note
.
author
.
id
,
editable:
note_editable
,
note_id:
note
.
id
}
}
.timeline-entry-inner
.timeline-entry-inner
.timeline-icon
.timeline-icon
-
if
note
.
system
-
if
note
.
system
...
...
changelogs/unreleased/30458-real-time-note-edits.yml
0 → 100644
View file @
cf34b07e
---
title
:
Update note edits in real-time
merge_request
:
author
:
spec/features/gitlab_flavored_markdown_spec.rb
View file @
cf34b07e
...
@@ -62,6 +62,8 @@ describe "GitLab Flavored Markdown", feature: true do
...
@@ -62,6 +62,8 @@ describe "GitLab Flavored Markdown", feature: true do
project:
project
,
project:
project
,
title:
"fix
#{
@other_issue
.
to_reference
}
"
,
title:
"fix
#{
@other_issue
.
to_reference
}
"
,
description:
"ask
#{
fred
.
to_reference
}
for details"
)
description:
"ask
#{
fred
.
to_reference
}
for details"
)
@note
=
create
(
:note_on_issue
,
noteable:
@issue
,
project:
@issue
.
project
,
note:
"Hello world"
)
end
end
it
"renders subject in issues#index"
do
it
"renders subject in issues#index"
do
...
@@ -81,14 +83,6 @@ describe "GitLab Flavored Markdown", feature: true do
...
@@ -81,14 +83,6 @@ describe "GitLab Flavored Markdown", feature: true do
expect
(
page
).
to
have_link
(
fred
.
to_reference
)
expect
(
page
).
to
have_link
(
fred
.
to_reference
)
end
end
it
"renders updated subject once edited somewhere else in issues#show"
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
@issue
)
@issue
.
update
(
title:
"fix
#{
@other_issue
.
to_reference
}
and update"
)
wait_for_vue_resource
expect
(
page
).
to
have_text
(
"fix
#{
@other_issue
.
to_reference
}
and update"
)
end
end
end
describe
"for merge requests"
do
describe
"for merge requests"
do
...
...
spec/features/issues/note_polling_spec.rb
View file @
cf34b07e
...
@@ -4,14 +4,77 @@ feature 'Issue notes polling', :feature, :js do
...
@@ -4,14 +4,77 @@ feature 'Issue notes polling', :feature, :js do
let
(
:project
)
{
create
(
:empty_project
,
:public
)
}
let
(
:project
)
{
create
(
:empty_project
,
:public
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
before
do
describe
'creates'
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
before
do
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
end
it
'displays the new comment'
do
note
=
create
(
:note
,
noteable:
issue
,
project:
project
,
note:
'Looks good!'
)
page
.
execute_script
(
'notes.refresh();'
)
expect
(
page
).
to
have_selector
(
"#note_
#{
note
.
id
}
"
,
text:
'Looks good!'
)
end
end
end
it
'should display the new comment'
do
describe
'updates'
do
note
=
create
(
:note
,
noteable:
issue
,
project:
project
,
note:
'Looks good!'
)
let
(
:user
)
{
create
(
:user
)
}
page
.
execute_script
(
'notes.refresh();'
)
let
(
:note_text
)
{
"Hello World"
}
let
(
:updated_text
)
{
"Bye World"
}
let!
(
:existing_note
)
{
create
(
:note
,
noteable:
issue
,
project:
project
,
author:
user
,
note:
note_text
)
}
before
do
login_as
(
user
)
visit
namespace_project_issue_path
(
project
.
namespace
,
project
,
issue
)
end
it
'displays the updated content'
do
expect
(
page
).
to
have_selector
(
"#note_
#{
existing_note
.
id
}
"
,
text:
note_text
)
update_note
(
existing_note
,
updated_text
)
expect
(
page
).
to
have_selector
(
"#note_
#{
existing_note
.
id
}
"
,
text:
updated_text
)
end
it
'when editing but have not changed anything, and an update comes in, show the updated content in the textarea'
do
find
(
"#note_
#{
existing_note
.
id
}
.js-note-edit"
).
click
expect
(
page
).
to
have_field
(
"note[note]"
,
with:
note_text
)
update_note
(
existing_note
,
updated_text
)
expect
(
page
).
to
have_field
(
"note[note]"
,
with:
updated_text
)
end
it
'when editing but you changed some things, and an update comes in, show a warning'
do
find
(
"#note_
#{
existing_note
.
id
}
.js-note-edit"
).
click
expect
(
page
).
to
have_selector
(
"#note_
#{
note
.
id
}
"
,
text:
'Looks good!'
)
expect
(
page
).
to
have_field
(
"note[note]"
,
with:
note_text
)
find
(
"#note_
#{
existing_note
.
id
}
.js-note-text"
).
set
(
'something random'
)
update_note
(
existing_note
,
updated_text
)
expect
(
page
).
to
have_selector
(
".alert"
)
end
it
'when editing but you changed some things, an update comes in, and you press cancel, show the updated content'
do
find
(
"#note_
#{
existing_note
.
id
}
.js-note-edit"
).
click
expect
(
page
).
to
have_field
(
"note[note]"
,
with:
note_text
)
find
(
"#note_
#{
existing_note
.
id
}
.js-note-text"
).
set
(
'something random'
)
update_note
(
existing_note
,
updated_text
)
find
(
"#note_
#{
existing_note
.
id
}
.note-edit-cancel"
).
click
expect
(
page
).
to
have_selector
(
"#note_
#{
existing_note
.
id
}
"
,
text:
updated_text
)
end
end
def
update_note
(
note
,
new_text
)
note
.
update
(
note:
new_text
)
page
.
execute_script
(
'notes.refresh();'
)
end
end
end
end
spec/javascripts/notes_spec.js
View file @
cf34b07e
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
/* global Notes */
require
(
'~/notes'
);
import
'vendor/autosize'
;
require
(
'vendor/autosize'
);
import
'~/gl_form'
;
require
(
'~/gl_form'
);
import
'~/lib/utils/text_utility'
;
require
(
'~/lib/utils/text_utility'
);
import
'~/render_gfm'
;
import
'~/render_math'
;
import
'~/notes'
;
(
function
()
{
(
function
()
{
window
.
gon
||
(
window
.
gon
=
{});
window
.
gon
||
(
window
.
gon
=
{});
...
@@ -80,35 +82,78 @@ require('~/lib/utils/text_utility');
...
@@ -80,35 +82,78 @@ require('~/lib/utils/text_utility');
beforeEach
(()
=>
{
beforeEach
(()
=>
{
note
=
{
note
=
{
id
:
1
,
discussion_html
:
null
,
discussion_html
:
null
,
valid
:
true
,
valid
:
true
,
html
:
'<div></div>'
,
note
:
'heya'
,
html
:
'<div>heya</div>'
,
};
};
$notesList
=
jasmine
.
createSpyObj
(
'$notesList'
,
[
'find'
]);
$notesList
=
jasmine
.
createSpyObj
(
'$notesList'
,
[
'find'
,
'append'
,
]);
notes
=
jasmine
.
createSpyObj
(
'notes'
,
[
notes
=
jasmine
.
createSpyObj
(
'notes'
,
[
'refresh'
,
'refresh'
,
'isNewNote'
,
'isNewNote'
,
'isUpdatedNote'
,
'collapseLongCommitList'
,
'collapseLongCommitList'
,
'updateNotesCount'
,
'updateNotesCount'
,
'putConflictEditWarningInPlace'
]);
]);
notes
.
taskList
=
jasmine
.
createSpyObj
(
'tasklist'
,
[
'init'
]);
notes
.
taskList
=
jasmine
.
createSpyObj
(
'tasklist'
,
[
'init'
]);
notes
.
note_ids
=
[];
notes
.
note_ids
=
[];
notes
.
updatedNotesTrackingMap
=
{};
spyOn
(
window
,
'$'
).
and
.
returnValue
(
$notesList
);
spyOn
(
gl
.
utils
,
'localTimeAgo'
);
spyOn
(
gl
.
utils
,
'localTimeAgo'
);
spyOn
(
Notes
,
'animateAppendNote'
);
spyOn
(
Notes
,
'animateAppendNote'
).
and
.
callThrough
();
notes
.
isNewNote
.
and
.
returnValue
(
true
);
spyOn
(
Notes
,
'animateUpdateNote'
).
and
.
callThrough
();
Notes
.
prototype
.
renderNote
.
call
(
notes
,
note
);
});
});
it
(
'should query for the notes list'
,
()
=>
{
describe
(
'when adding note'
,
()
=>
{
expect
(
window
.
$
).
toHaveBeenCalledWith
(
'ul.main-notes-list'
);
it
(
'should call .animateAppendNote'
,
()
=>
{
notes
.
isNewNote
.
and
.
returnValue
(
true
);
Notes
.
prototype
.
renderNote
.
call
(
notes
,
note
,
null
,
$notesList
);
expect
(
Notes
.
animateAppendNote
).
toHaveBeenCalledWith
(
note
.
html
,
$notesList
);
});
});
});
it
(
'should call .animateAppendNote'
,
()
=>
{
describe
(
'when note was edited'
,
()
=>
{
expect
(
Notes
.
animateAppendNote
).
toHaveBeenCalledWith
(
note
.
html
,
$notesList
);
it
(
'should call .animateUpdateNote'
,
()
=>
{
notes
.
isUpdatedNote
.
and
.
returnValue
(
true
);
const
$note
=
$
(
'<div>'
);
$notesList
.
find
.
and
.
returnValue
(
$note
);
Notes
.
prototype
.
renderNote
.
call
(
notes
,
note
,
null
,
$notesList
);
expect
(
Notes
.
animateUpdateNote
).
toHaveBeenCalledWith
(
note
.
html
,
$note
);
});
describe
(
'while editing'
,
()
=>
{
it
(
'should update textarea if nothing has been touched'
,
()
=>
{
notes
.
isUpdatedNote
.
and
.
returnValue
(
true
);
const
$note
=
$
(
`<div class="is-editing">
<div class="original-note-content">initial</div>
<textarea class="js-note-text">initial</textarea>
</div>`
);
$notesList
.
find
.
and
.
returnValue
(
$note
);
Notes
.
prototype
.
renderNote
.
call
(
notes
,
note
,
null
,
$notesList
);
expect
(
$note
.
find
(
'.js-note-text'
).
val
()).
toEqual
(
note
.
note
);
});
it
(
'should call .putConflictEditWarningInPlace'
,
()
=>
{
notes
.
isUpdatedNote
.
and
.
returnValue
(
true
);
const
$note
=
$
(
`<div class="is-editing">
<div class="original-note-content">initial</div>
<textarea class="js-note-text">different</textarea>
</div>`
);
$notesList
.
find
.
and
.
returnValue
(
$note
);
Notes
.
prototype
.
renderNote
.
call
(
notes
,
note
,
null
,
$notesList
);
expect
(
notes
.
putConflictEditWarningInPlace
).
toHaveBeenCalledWith
(
note
,
$note
);
});
});
});
});
});
});
...
@@ -147,14 +192,12 @@ require('~/lib/utils/text_utility');
...
@@ -147,14 +192,12 @@ require('~/lib/utils/text_utility');
});
});
describe
(
'Discussion root note'
,
()
=>
{
describe
(
'Discussion root note'
,
()
=>
{
let
$notesList
;
let
body
;
let
body
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
body
=
jasmine
.
createSpyObj
(
'body'
,
[
'attr'
]);
body
=
jasmine
.
createSpyObj
(
'body'
,
[
'attr'
]);
discussionContainer
=
{
length
:
0
};
discussionContainer
=
{
length
:
0
};
spyOn
(
window
,
'$'
).
and
.
returnValues
(
discussionContainer
,
body
,
$notesList
);
$form
.
closest
.
and
.
returnValues
(
row
,
$form
);
$form
.
closest
.
and
.
returnValues
(
row
,
$form
);
$form
.
find
.
and
.
returnValues
(
discussionContainer
);
$form
.
find
.
and
.
returnValues
(
discussionContainer
);
body
.
attr
.
and
.
returnValue
(
''
);
body
.
attr
.
and
.
returnValue
(
''
);
...
@@ -162,12 +205,8 @@ require('~/lib/utils/text_utility');
...
@@ -162,12 +205,8 @@ require('~/lib/utils/text_utility');
Notes
.
prototype
.
renderDiscussionNote
.
call
(
notes
,
note
,
$form
);
Notes
.
prototype
.
renderDiscussionNote
.
call
(
notes
,
note
,
$form
);
});
});
it
(
'should query for the notes list'
,
()
=>
{
expect
(
window
.
$
.
calls
.
argsFor
(
2
)).
toEqual
([
'ul.main-notes-list'
]);
});
it
(
'should call Notes.animateAppendNote'
,
()
=>
{
it
(
'should call Notes.animateAppendNote'
,
()
=>
{
expect
(
Notes
.
animateAppendNote
).
toHaveBeenCalledWith
(
note
.
discussion_html
,
$
notesList
);
expect
(
Notes
.
animateAppendNote
).
toHaveBeenCalledWith
(
note
.
discussion_html
,
$
(
'.main-notes-list'
)
);
});
});
});
});
...
@@ -175,16 +214,12 @@ require('~/lib/utils/text_utility');
...
@@ -175,16 +214,12 @@ require('~/lib/utils/text_utility');
beforeEach
(()
=>
{
beforeEach
(()
=>
{
discussionContainer
=
{
length
:
1
};
discussionContainer
=
{
length
:
1
};
spyOn
(
window
,
'$'
).
and
.
returnValues
(
discussionContainer
);
$form
.
closest
.
and
.
returnValues
(
row
,
$form
);
$form
.
closest
.
and
.
returnValues
(
row
);
$form
.
find
.
and
.
returnValues
(
discussionContainer
);
Notes
.
prototype
.
renderDiscussionNote
.
call
(
notes
,
note
,
$form
);
Notes
.
prototype
.
renderDiscussionNote
.
call
(
notes
,
note
,
$form
);
});
});
it
(
'should query foor the discussion container'
,
()
=>
{
expect
(
window
.
$
).
toHaveBeenCalledWith
(
`.notes[data-discussion-id="
${
note
.
discussion_id
}
"]`
);
});
it
(
'should call Notes.animateAppendNote'
,
()
=>
{
it
(
'should call Notes.animateAppendNote'
,
()
=>
{
expect
(
Notes
.
animateAppendNote
).
toHaveBeenCalledWith
(
note
.
html
,
discussionContainer
);
expect
(
Notes
.
animateAppendNote
).
toHaveBeenCalledWith
(
note
.
html
,
discussionContainer
);
});
});
...
@@ -193,35 +228,45 @@ require('~/lib/utils/text_utility');
...
@@ -193,35 +228,45 @@ require('~/lib/utils/text_utility');
describe
(
'animateAppendNote'
,
()
=>
{
describe
(
'animateAppendNote'
,
()
=>
{
let
noteHTML
;
let
noteHTML
;
let
$note
;
let
$notesList
;
let
$notesList
;
let
$resultantNote
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
noteHTML
=
'<div></div>'
;
noteHTML
=
'<div></div>'
;
$note
=
jasmine
.
createSpyObj
(
'$note'
,
[
'addClass'
,
'renderGFM'
,
'removeClass'
]);
$notesList
=
jasmine
.
createSpyObj
(
'$notesList'
,
[
'append'
]);
$notesList
=
jasmine
.
createSpyObj
(
'$notesList'
,
[
'append'
]);
spyOn
(
window
,
'$'
).
and
.
returnValue
(
$note
);
$resultantNote
=
Notes
.
animateAppendNote
(
noteHTML
,
$notesList
);
spyOn
(
window
,
'setTimeout'
).
and
.
callThrough
();
});
$note
.
addClass
.
and
.
returnValue
(
$note
);
$note
.
renderGFM
.
and
.
returnValue
(
$note
);
Notes
.
animateAppendNote
(
noteHTML
,
$notesList
);
it
(
'should have `fade-in` class'
,
()
=>
{
expect
(
$resultantNote
.
hasClass
(
'fade-in'
)).
toEqual
(
true
);
});
});
it
(
'should
init the note jquery objec
t'
,
()
=>
{
it
(
'should
append note to the notes lis
t'
,
()
=>
{
expect
(
window
.
$
).
toHaveBeenCalledWith
(
noteHTML
);
expect
(
$notesList
.
append
).
toHaveBeenCalledWith
(
$resultantNote
);
});
});
});
describe
(
'animateUpdateNote'
,
()
=>
{
let
noteHTML
;
let
$note
;
let
$updatedNote
;
it
(
'should call addClass'
,
()
=>
{
beforeEach
(()
=>
{
expect
(
$note
.
addClass
).
toHaveBeenCalledWith
(
'fade-in'
);
noteHTML
=
'<div></div>'
;
$note
=
jasmine
.
createSpyObj
(
'$note'
,
[
'replaceWith'
]);
$updatedNote
=
Notes
.
animateUpdateNote
(
noteHTML
,
$note
);
});
});
it
(
'should call renderGFM'
,
()
=>
{
expect
(
$note
.
renderGFM
).
toHaveBeenCalledWith
();
it
(
'should have `fade-in` class'
,
()
=>
{
expect
(
$updatedNote
.
hasClass
(
'fade-in'
)).
toEqual
(
true
);
});
});
it
(
'should
append note to the notes list
'
,
()
=>
{
it
(
'should
call replaceWith on $note
'
,
()
=>
{
expect
(
$note
sList
.
append
).
toHaveBeenCalledWith
(
$n
ote
);
expect
(
$note
.
replaceWith
).
toHaveBeenCalledWith
(
$updatedN
ote
);
});
});
});
});
});
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment