Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Léo-Paul Géneau
gitlab-ce
Commits
b54203f0
Commit
b54203f0
authored
Oct 07, 2017
by
Felipe Artur
Committed by
Jacob Schatz
Oct 07, 2017
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Commenting on image diffs
parent
b4f9dc48
Changes
77
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
77 changed files
with
3318 additions
and
270 deletions
+3318
-270
app/assets/images/icon_image_comment.svg
app/assets/images/icon_image_comment.svg
+1
-0
app/assets/images/icon_image_comment@2x.svg
app/assets/images/icon_image_comment@2x.svg
+1
-0
app/assets/javascripts/commit.js
app/assets/javascripts/commit.js
+0
-12
app/assets/javascripts/commit/file.js
app/assets/javascripts/commit/file.js
+0
-14
app/assets/javascripts/commit/image_file.js
app/assets/javascripts/commit/image_file.js
+5
-8
app/assets/javascripts/diff.js
app/assets/javascripts/diff.js
+4
-1
app/assets/javascripts/diff_notes/components/jump_to_discussion.js
...s/javascripts/diff_notes/components/jump_to_discussion.js
+8
-1
app/assets/javascripts/dispatcher.js
app/assets/javascripts/dispatcher.js
+0
-2
app/assets/javascripts/image_diff/helpers/badge_helper.js
app/assets/javascripts/image_diff/helpers/badge_helper.js
+38
-0
app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
...avascripts/image_diff/helpers/comment_indicator_helper.js
+58
-0
app/assets/javascripts/image_diff/helpers/dom_helper.js
app/assets/javascripts/image_diff/helpers/dom_helper.js
+44
-0
app/assets/javascripts/image_diff/helpers/index.js
app/assets/javascripts/image_diff/helpers/index.js
+25
-0
app/assets/javascripts/image_diff/helpers/utils_helper.js
app/assets/javascripts/image_diff/helpers/utils_helper.js
+95
-0
app/assets/javascripts/image_diff/image_badge.js
app/assets/javascripts/image_diff/image_badge.js
+23
-0
app/assets/javascripts/image_diff/image_diff.js
app/assets/javascripts/image_diff/image_diff.js
+143
-0
app/assets/javascripts/image_diff/init_discussion_tab.js
app/assets/javascripts/image_diff/init_discussion_tab.js
+12
-0
app/assets/javascripts/image_diff/replaced_image_diff.js
app/assets/javascripts/image_diff/replaced_image_diff.js
+92
-0
app/assets/javascripts/image_diff/view_types.js
app/assets/javascripts/image_diff/view_types.js
+9
-0
app/assets/javascripts/lib/utils/image_utility.js
app/assets/javascripts/lib/utils/image_utility.js
+5
-0
app/assets/javascripts/main.js
app/assets/javascripts/main.js
+0
-3
app/assets/javascripts/merge_request_tabs.js
app/assets/javascripts/merge_request_tabs.js
+4
-0
app/assets/javascripts/notes.js
app/assets/javascripts/notes.js
+119
-9
app/assets/javascripts/single_file_diff.js
app/assets/javascripts/single_file_diff.js
+6
-1
app/assets/stylesheets/framework/buttons.scss
app/assets/stylesheets/framework/buttons.scss
+22
-0
app/assets/stylesheets/framework/timeline.scss
app/assets/stylesheets/framework/timeline.scss
+5
-1
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+7
-0
app/assets/stylesheets/pages/diff.scss
app/assets/stylesheets/pages/diff.scss
+165
-3
app/assets/stylesheets/pages/note_form.scss
app/assets/stylesheets/pages/note_form.scss
+4
-1
app/assets/stylesheets/pages/notes.scss
app/assets/stylesheets/pages/notes.scss
+1
-18
app/controllers/concerns/notes_actions.rb
app/controllers/concerns/notes_actions.rb
+8
-3
app/models/concerns/discussion_on_diff.rb
app/models/concerns/discussion_on_diff.rb
+4
-0
app/models/diff_discussion.rb
app/models/diff_discussion.rb
+2
-0
app/models/diff_note.rb
app/models/diff_note.rb
+12
-2
app/models/discussion.rb
app/models/discussion.rb
+4
-0
app/models/legacy_diff_discussion.rb
app/models/legacy_diff_discussion.rb
+8
-0
app/models/note.rb
app/models/note.rb
+12
-4
app/views/discussions/_diff_discussion.html.haml
app/views/discussions/_diff_discussion.html.haml
+10
-6
app/views/discussions/_diff_with_notes.html.haml
app/views/discussions/_diff_with_notes.html.haml
+20
-11
app/views/discussions/_notes.html.haml
app/views/discussions/_notes.html.haml
+16
-3
app/views/projects/diffs/_image_diff_frame.html.haml
app/views/projects/diffs/_image_diff_frame.html.haml
+5
-0
app/views/projects/diffs/_replaced_image_diff.html.haml
app/views/projects/diffs/_replaced_image_diff.html.haml
+61
-0
app/views/projects/diffs/_single_image_diff.html.haml
app/views/projects/diffs/_single_image_diff.html.haml
+16
-0
app/views/projects/diffs/viewers/_image.html.haml
app/views/projects/diffs/viewers/_image.html.haml
+8
-62
app/views/shared/notes/_form.html.haml
app/views/shared/notes/_form.html.haml
+18
-17
app/views/shared/notes/_note.html.haml
app/views/shared/notes/_note.html.haml
+14
-1
changelogs/unreleased/issue_35873.yml
changelogs/unreleased/issue_35873.yml
+5
-0
lib/gitlab/diff/file.rb
lib/gitlab/diff/file.rb
+22
-7
lib/gitlab/diff/formatters/base_formatter.rb
lib/gitlab/diff/formatters/base_formatter.rb
+61
-0
lib/gitlab/diff/formatters/image_formatter.rb
lib/gitlab/diff/formatters/image_formatter.rb
+43
-0
lib/gitlab/diff/formatters/text_formatter.rb
lib/gitlab/diff/formatters/text_formatter.rb
+49
-0
lib/gitlab/diff/image_point.rb
lib/gitlab/diff/image_point.rb
+23
-0
lib/gitlab/diff/position.rb
lib/gitlab/diff/position.rb
+38
-52
spec/factories/merge_requests.rb
spec/factories/merge_requests.rb
+5
-0
spec/features/merge_requests/diffs_spec.rb
spec/features/merge_requests/diffs_spec.rb
+6
-2
spec/features/merge_requests/image_diff_notes.rb
spec/features/merge_requests/image_diff_notes.rb
+196
-0
spec/features/merge_requests/user_posts_diff_notes_spec.rb
spec/features/merge_requests/user_posts_diff_notes_spec.rb
+1
-0
spec/javascripts/image_diff/helpers/badge_helper_spec.js
spec/javascripts/image_diff/helpers/badge_helper_spec.js
+132
-0
spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
...ripts/image_diff/helpers/comment_indicator_helper_spec.js
+139
-0
spec/javascripts/image_diff/helpers/dom_helper_spec.js
spec/javascripts/image_diff/helpers/dom_helper_spec.js
+118
-0
spec/javascripts/image_diff/helpers/utils_helper_spec.js
spec/javascripts/image_diff/helpers/utils_helper_spec.js
+207
-0
spec/javascripts/image_diff/image_badge_spec.js
spec/javascripts/image_diff/image_badge_spec.js
+84
-0
spec/javascripts/image_diff/image_diff_spec.js
spec/javascripts/image_diff/image_diff_spec.js
+361
-0
spec/javascripts/image_diff/init_discussion_tab_spec.js
spec/javascripts/image_diff/init_discussion_tab_spec.js
+37
-0
spec/javascripts/image_diff/mock_data.js
spec/javascripts/image_diff/mock_data.js
+28
-0
spec/javascripts/image_diff/replaced_image_diff_spec.js
spec/javascripts/image_diff/replaced_image_diff_spec.js
+312
-0
spec/javascripts/image_diff/view_types_spec.js
spec/javascripts/image_diff/view_types_spec.js
+24
-0
spec/javascripts/lib/utils/image_utility_spec.js
spec/javascripts/lib/utils/image_utility_spec.js
+32
-0
spec/lib/gitlab/diff/formatters/image_formatter_spec.rb
spec/lib/gitlab/diff/formatters/image_formatter_spec.rb
+20
-0
spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
+42
-0
spec/lib/gitlab/diff/position_spec.rb
spec/lib/gitlab/diff/position_spec.rb
+69
-16
spec/lib/gitlab/diff/position_tracer_spec.rb
spec/lib/gitlab/diff/position_tracer_spec.rb
+14
-2
spec/models/diff_note_spec.rb
spec/models/diff_note_spec.rb
+37
-3
spec/models/note_spec.rb
spec/models/note_spec.rb
+50
-0
spec/services/discussions/update_diff_position_service_spec.rb
...services/discussions/update_diff_position_service_spec.rb
+3
-3
spec/support/features/discussion_comments_shared_example.rb
spec/support/features/discussion_comments_shared_example.rb
+1
-1
spec/support/shared_examples/position_formatters.rb
spec/support/shared_examples/position_formatters.rb
+43
-0
spec/support/test_env.rb
spec/support/test_env.rb
+2
-1
No files found.
app/assets/images/icon_image_comment.svg
0 → 100644
View file @
b54203f0
<svg
width=
"24"
height=
"30"
viewBox=
"0 0 24 30"
xmlns=
"http://www.w3.org/2000/svg"
><title>
cursor
</title><g
fill=
"none"
fill-rule=
"evenodd"
><path
d=
"M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z"
fill=
"#1F78D1"
fill-rule=
"nonzero"
/><path
d=
"M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z"
fill=
"#FFF"
/><path
d=
"M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z"
fill=
"#1F78D1"
fill-rule=
"nonzero"
/></g></svg>
app/assets/images/icon_image_comment@2x.svg
0 → 100644
View file @
b54203f0
<svg
width=
"48"
height=
"60"
viewBox=
"0 0 48 60"
xmlns=
"http://www.w3.org/2000/svg"
><title>
cursor_2x
</title><g
fill=
"none"
fill-rule=
"evenodd"
><path
d=
"M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z"
fill=
"#1F78D1"
fill-rule=
"nonzero"
/><path
d=
"M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z"
fill=
"#FFF"
/><path
d=
"M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z"
fill=
"#1F78D1"
fill-rule=
"nonzero"
/></g></svg>
app/assets/javascripts/commit.js
deleted
100644 → 0
View file @
b4f9dc48
/* eslint-disable func-names, space-before-function-paren, wrap-iife */
/* global CommitFile */
window
.
Commit
=
(
function
()
{
function
Commit
()
{
$
(
'
.files .diff-file
'
).
each
(
function
()
{
return
new
CommitFile
(
this
);
});
}
return
Commit
;
})();
app/assets/javascripts/commit/file.js
deleted
100644 → 0
View file @
b4f9dc48
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */
/* global ImageFile */
(
function
()
{
this
.
CommitFile
=
(
function
()
{
function
CommitFile
(
file
)
{
if
(
$
(
'
.image
'
,
file
).
length
)
{
new
gl
.
ImageFile
(
file
);
}
}
return
CommitFile
;
})();
}).
call
(
window
);
app/assets/javascripts/commit/image_file.js
View file @
b54203f0
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
import
'
vendor/jquery.waitforimages
'
;
(
function
()
{
(
function
()
{
gl
.
ImageFile
=
(
function
()
{
gl
.
ImageFile
=
(
function
()
{
var
prepareFrames
;
var
prepareFrames
;
...
@@ -17,15 +19,10 @@
...
@@ -17,15 +19,10 @@
// Load two-up view after images are loaded
// Load two-up view after images are loaded
// so that we can display the correct width and height information
// so that we can display the correct width and height information
const
images
=
$
(
'
.two-up.view img
'
,
_this
.
file
);
const
$images
=
$
(
'
.two-up.view img
'
,
_this
.
file
);
let
loadedCount
=
0
;
images
.
on
(
'
load
'
,
()
=>
{
loadedCount
+=
1
;
if
(
loadedCount
===
images
.
length
)
{
$images
.
waitForImages
(
function
()
{
_this
.
initView
(
'
two-up
'
);
_this
.
initView
(
'
two-up
'
);
}
});
});
});
});
};
};
...
...
app/assets/javascripts/diff.js
View file @
b54203f0
...
@@ -3,6 +3,7 @@
...
@@ -3,6 +3,7 @@
import
'
./lib/utils/url_utility
'
;
import
'
./lib/utils/url_utility
'
;
import
FilesCommentButton
from
'
./files_comment_button
'
;
import
FilesCommentButton
from
'
./files_comment_button
'
;
import
SingleFileDiff
from
'
./single_file_diff
'
;
import
SingleFileDiff
from
'
./single_file_diff
'
;
import
imageDiffHelper
from
'
./image_diff/helpers/index
'
;
const
UNFOLD_COUNT
=
20
;
const
UNFOLD_COUNT
=
20
;
let
isBound
=
false
;
let
isBound
=
false
;
...
@@ -20,7 +21,9 @@ class Diff {
...
@@ -20,7 +21,9 @@ class Diff {
const
tab
=
document
.
getElementById
(
'
diffs
'
);
const
tab
=
document
.
getElementById
(
'
diffs
'
);
if
(
!
tab
||
(
tab
&&
tab
.
dataset
&&
tab
.
dataset
.
isLocked
!==
''
))
FilesCommentButton
.
init
(
$diffFile
);
if
(
!
tab
||
(
tab
&&
tab
.
dataset
&&
tab
.
dataset
.
isLocked
!==
''
))
FilesCommentButton
.
init
(
$diffFile
);
$diffFile
.
each
((
index
,
file
)
=>
new
gl
.
ImageFile
(
file
));
const
firstFile
=
$
(
'
.files
'
).
first
().
get
(
0
);
const
canCreateNote
=
firstFile
&&
firstFile
.
hasAttribute
(
'
data-can-create-note
'
);
$diffFile
.
each
((
index
,
file
)
=>
imageDiffHelper
.
initImageDiff
(
file
,
canCreateNote
));
if
(
!
isBound
)
{
if
(
!
isBound
)
{
$
(
document
)
$
(
document
)
...
...
app/assets/javascripts/diff_notes/components/jump_to_discussion.js
View file @
b54203f0
...
@@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({
...
@@ -171,7 +171,14 @@ const JumpToDiscussion = Vue.extend({
// When jumping between unresolved discussions on the diffs tab, we show them.
// When jumping between unresolved discussions on the diffs tab, we show them.
$target
.
closest
(
"
.content
"
).
show
();
$target
.
closest
(
"
.content
"
).
show
();
$target
=
$target
.
closest
(
"
tr.notes_holder
"
);
const
$notesHolder
=
$target
.
closest
(
"
tr.notes_holder
"
);
// Image diff discussions does not use notes_holder
// so we should keep original $target value in those cases
if
(
$notesHolder
.
length
>
0
)
{
$target
=
$notesHolder
;
}
$target
.
show
();
$target
.
show
();
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
...
...
app/assets/javascripts/dispatcher.js
View file @
b54203f0
...
@@ -7,7 +7,6 @@
...
@@ -7,7 +7,6 @@
/* global IssuableForm */
/* global IssuableForm */
/* global LabelsSelect */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global MilestoneSelect */
/* global Commit */
/* global CommitsList */
/* global CommitsList */
/* global NewBranchForm */
/* global NewBranchForm */
/* global NotificationsForm */
/* global NotificationsForm */
...
@@ -316,7 +315,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
...
@@ -316,7 +315,6 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
new
gl
.
Activities
();
new
gl
.
Activities
();
break
;
break
;
case
'
projects:commit:show
'
:
case
'
projects:commit:show
'
:
new
Commit
();
new
gl
.
Diff
();
new
gl
.
Diff
();
new
ZenMode
();
new
ZenMode
();
shortcut_handler
=
new
ShortcutsNavigation
();
shortcut_handler
=
new
ShortcutsNavigation
();
...
...
app/assets/javascripts/image_diff/helpers/badge_helper.js
0 → 100644
View file @
b54203f0
export
function
createImageBadge
(
noteId
,
{
x
,
y
},
classNames
=
[])
{
const
buttonEl
=
document
.
createElement
(
'
button
'
);
const
classList
=
classNames
.
concat
([
'
js-image-badge
'
]);
classList
.
forEach
(
className
=>
buttonEl
.
classList
.
add
(
className
));
buttonEl
.
setAttribute
(
'
type
'
,
'
button
'
);
buttonEl
.
setAttribute
(
'
disabled
'
,
true
);
buttonEl
.
dataset
.
noteId
=
noteId
;
buttonEl
.
style
.
left
=
`
${
x
}
px`
;
buttonEl
.
style
.
top
=
`
${
y
}
px`
;
return
buttonEl
;
}
export
function
addImageBadge
(
containerEl
,
{
coordinate
,
badgeText
,
noteId
})
{
const
buttonEl
=
createImageBadge
(
noteId
,
coordinate
,
[
'
badge
'
]);
buttonEl
.
innerText
=
badgeText
;
containerEl
.
appendChild
(
buttonEl
);
}
export
function
addImageCommentBadge
(
containerEl
,
{
coordinate
,
noteId
})
{
const
buttonEl
=
createImageBadge
(
noteId
,
coordinate
,
[
'
image-comment-badge
'
,
'
inverted
'
]);
const
iconEl
=
document
.
createElement
(
'
i
'
);
iconEl
.
className
=
'
fa fa-comment-o
'
;
iconEl
.
setAttribute
(
'
aria-label
'
,
'
comment
'
);
buttonEl
.
appendChild
(
iconEl
);
containerEl
.
appendChild
(
buttonEl
);
}
export
function
addAvatarBadge
(
el
,
event
)
{
const
{
noteId
,
badgeNumber
}
=
event
.
detail
;
// Add badge to new comment
const
avatarBadgeEl
=
el
.
querySelector
(
`#
${
noteId
}
.badge`
);
avatarBadgeEl
.
innerText
=
badgeNumber
;
avatarBadgeEl
.
classList
.
remove
(
'
hidden
'
);
}
app/assets/javascripts/image_diff/helpers/comment_indicator_helper.js
0 → 100644
View file @
b54203f0
export
function
addCommentIndicator
(
containerEl
,
{
x
,
y
})
{
const
buttonEl
=
document
.
createElement
(
'
button
'
);
buttonEl
.
classList
.
add
(
'
btn-transparent
'
);
buttonEl
.
classList
.
add
(
'
comment-indicator
'
);
buttonEl
.
setAttribute
(
'
type
'
,
'
button
'
);
buttonEl
.
style
.
left
=
`
${
x
}
px`
;
buttonEl
.
style
.
top
=
`
${
y
}
px`
;
buttonEl
.
innerHTML
=
gl
.
utils
.
spriteIcon
(
'
image-comment-dark
'
);
containerEl
.
appendChild
(
buttonEl
);
}
export
function
removeCommentIndicator
(
imageFrameEl
)
{
const
commentIndicatorEl
=
imageFrameEl
.
querySelector
(
'
.comment-indicator
'
);
const
imageEl
=
imageFrameEl
.
querySelector
(
'
img
'
);
const
willRemove
=
!!
commentIndicatorEl
;
let
meta
=
{};
if
(
willRemove
)
{
meta
=
{
x
:
parseInt
(
commentIndicatorEl
.
style
.
left
,
10
),
y
:
parseInt
(
commentIndicatorEl
.
style
.
top
,
10
),
image
:
{
width
:
imageEl
.
width
,
height
:
imageEl
.
height
,
},
};
commentIndicatorEl
.
remove
();
}
return
Object
.
assign
({},
meta
,
{
removed
:
willRemove
,
});
}
export
function
showCommentIndicator
(
imageFrameEl
,
coordinate
)
{
const
{
x
,
y
}
=
coordinate
;
const
commentIndicatorEl
=
imageFrameEl
.
querySelector
(
'
.comment-indicator
'
);
if
(
commentIndicatorEl
)
{
commentIndicatorEl
.
style
.
left
=
`
${
x
}
px`
;
commentIndicatorEl
.
style
.
top
=
`
${
y
}
px`
;
}
else
{
addCommentIndicator
(
imageFrameEl
,
coordinate
);
}
}
export
function
commentIndicatorOnClick
(
event
)
{
// Prevent from triggering onAddImageDiffNote in notes.js
event
.
stopPropagation
();
const
buttonEl
=
event
.
currentTarget
;
const
diffViewerEl
=
buttonEl
.
closest
(
'
.diff-viewer
'
);
const
textareaEl
=
diffViewerEl
.
querySelector
(
'
.note-container .note-textarea
'
);
textareaEl
.
focus
();
}
app/assets/javascripts/image_diff/helpers/dom_helper.js
0 → 100644
View file @
b54203f0
export
function
setPositionDataAttribute
(
el
,
options
)
{
// Update position data attribute so that the
// new comment form can use this data for ajax request
const
{
x
,
y
,
width
,
height
}
=
options
;
const
position
=
el
.
dataset
.
position
;
const
positionObject
=
Object
.
assign
({},
JSON
.
parse
(
position
),
{
x
,
y
,
width
,
height
,
});
el
.
setAttribute
(
'
data-position
'
,
JSON
.
stringify
(
positionObject
));
}
export
function
updateDiscussionAvatarBadgeNumber
(
discussionEl
,
newBadgeNumber
)
{
const
avatarBadgeEl
=
discussionEl
.
querySelector
(
'
.image-diff-avatar-link .badge
'
);
avatarBadgeEl
.
innerText
=
newBadgeNumber
;
}
export
function
updateDiscussionBadgeNumber
(
discussionEl
,
newBadgeNumber
)
{
const
discussionBadgeEl
=
discussionEl
.
querySelector
(
'
.badge
'
);
discussionBadgeEl
.
innerText
=
newBadgeNumber
;
}
export
function
toggleCollapsed
(
event
)
{
const
toggleButtonEl
=
event
.
currentTarget
;
const
discussionNotesEl
=
toggleButtonEl
.
closest
(
'
.discussion-notes
'
);
const
formEl
=
discussionNotesEl
.
querySelector
(
'
.discussion-form
'
);
const
isCollapsed
=
discussionNotesEl
.
classList
.
contains
(
'
collapsed
'
);
if
(
isCollapsed
)
{
discussionNotesEl
.
classList
.
remove
(
'
collapsed
'
);
}
else
{
discussionNotesEl
.
classList
.
add
(
'
collapsed
'
);
}
// Override the inline display style set in notes.js
if
(
formEl
&&
!
isCollapsed
)
{
formEl
.
style
.
display
=
'
none
'
;
}
else
if
(
formEl
&&
isCollapsed
)
{
formEl
.
style
.
display
=
'
block
'
;
}
}
app/assets/javascripts/image_diff/helpers/index.js
0 → 100644
View file @
b54203f0
import
*
as
badgeHelper
from
'
./badge_helper
'
;
import
*
as
commentIndicatorHelper
from
'
./comment_indicator_helper
'
;
import
*
as
domHelper
from
'
./dom_helper
'
;
import
*
as
utilsHelper
from
'
./utils_helper
'
;
export
default
{
addCommentIndicator
:
commentIndicatorHelper
.
addCommentIndicator
,
removeCommentIndicator
:
commentIndicatorHelper
.
removeCommentIndicator
,
showCommentIndicator
:
commentIndicatorHelper
.
showCommentIndicator
,
commentIndicatorOnClick
:
commentIndicatorHelper
.
commentIndicatorOnClick
,
addImageBadge
:
badgeHelper
.
addImageBadge
,
addImageCommentBadge
:
badgeHelper
.
addImageCommentBadge
,
addAvatarBadge
:
badgeHelper
.
addAvatarBadge
,
setPositionDataAttribute
:
domHelper
.
setPositionDataAttribute
,
updateDiscussionAvatarBadgeNumber
:
domHelper
.
updateDiscussionAvatarBadgeNumber
,
updateDiscussionBadgeNumber
:
domHelper
.
updateDiscussionBadgeNumber
,
toggleCollapsed
:
domHelper
.
toggleCollapsed
,
resizeCoordinatesToImageElement
:
utilsHelper
.
resizeCoordinatesToImageElement
,
generateBadgeFromDiscussionDOM
:
utilsHelper
.
generateBadgeFromDiscussionDOM
,
getTargetSelection
:
utilsHelper
.
getTargetSelection
,
initImageDiff
:
utilsHelper
.
initImageDiff
,
};
app/assets/javascripts/image_diff/helpers/utils_helper.js
0 → 100644
View file @
b54203f0
import
ImageBadge
from
'
../image_badge
'
;
import
ImageDiff
from
'
../image_diff
'
;
import
ReplacedImageDiff
from
'
../replaced_image_diff
'
;
import
'
../../commit/image_file
'
;
export
function
resizeCoordinatesToImageElement
(
imageEl
,
meta
)
{
const
{
x
,
y
,
width
,
height
}
=
meta
;
const
imageWidth
=
imageEl
.
width
;
const
imageHeight
=
imageEl
.
height
;
const
widthRatio
=
imageWidth
/
width
;
const
heightRatio
=
imageHeight
/
height
;
return
{
x
:
Math
.
round
(
x
*
widthRatio
),
y
:
Math
.
round
(
y
*
heightRatio
),
width
:
imageWidth
,
height
:
imageHeight
,
};
}
export
function
generateBadgeFromDiscussionDOM
(
imageFrameEl
,
discussionEl
)
{
const
position
=
JSON
.
parse
(
discussionEl
.
dataset
.
position
);
const
firstNoteEl
=
discussionEl
.
querySelector
(
'
.note
'
);
const
badge
=
new
ImageBadge
({
actual
:
position
,
imageEl
:
imageFrameEl
.
querySelector
(
'
img
'
),
noteId
:
firstNoteEl
.
id
,
discussionId
:
discussionEl
.
dataset
.
discussionId
,
});
return
badge
;
}
export
function
getTargetSelection
(
event
)
{
const
containerEl
=
event
.
currentTarget
;
const
imageEl
=
containerEl
.
querySelector
(
'
img
'
);
const
x
=
event
.
offsetX
;
const
y
=
event
.
offsetY
;
const
width
=
imageEl
.
width
;
const
height
=
imageEl
.
height
;
const
actualWidth
=
imageEl
.
naturalWidth
;
const
actualHeight
=
imageEl
.
naturalHeight
;
const
widthRatio
=
actualWidth
/
width
;
const
heightRatio
=
actualHeight
/
height
;
// Browser will include the frame as a clickable target,
// which would result in potential 1px out of bounds value
// This bound the coordinates to inside the frame
const
normalizedX
=
Math
.
max
(
0
,
x
)
&&
Math
.
min
(
x
,
width
);
const
normalizedY
=
Math
.
max
(
0
,
y
)
&&
Math
.
min
(
y
,
height
);
return
{
browser
:
{
x
:
normalizedX
,
y
:
normalizedY
,
width
,
height
,
},
actual
:
{
// Round x, y so that we don't need to deal with decimals
x
:
Math
.
round
(
normalizedX
*
widthRatio
),
y
:
Math
.
round
(
normalizedY
*
heightRatio
),
width
:
actualWidth
,
height
:
actualHeight
,
},
};
}
export
function
initImageDiff
(
fileEl
,
canCreateNote
,
renderCommentBadge
)
{
const
options
=
{
canCreateNote
,
renderCommentBadge
,
};
let
diff
;
// ImageFile needs to be invoked before initImageDiff so that badges
// can mount to the correct location
new
gl
.
ImageFile
(
fileEl
);
// eslint-disable-line no-new
if
(
fileEl
.
querySelector
(
'
.diff-file .js-single-image
'
))
{
diff
=
new
ImageDiff
(
fileEl
,
options
);
diff
.
init
();
}
else
if
(
fileEl
.
querySelector
(
'
.diff-file .js-replaced-image
'
))
{
diff
=
new
ReplacedImageDiff
(
fileEl
,
options
);
diff
.
init
();
}
return
diff
;
}
app/assets/javascripts/image_diff/image_badge.js
0 → 100644
View file @
b54203f0
import
imageDiffHelper
from
'
./helpers/index
'
;
const
defaultMeta
=
{
x
:
0
,
y
:
0
,
width
:
0
,
height
:
0
,
};
export
default
class
ImageBadge
{
constructor
(
options
)
{
const
{
noteId
,
discussionId
}
=
options
;
this
.
actual
=
options
.
actual
||
defaultMeta
;
this
.
browser
=
options
.
browser
||
defaultMeta
;
this
.
noteId
=
noteId
;
this
.
discussionId
=
discussionId
;
if
(
options
.
imageEl
&&
!
options
.
browser
)
{
this
.
browser
=
imageDiffHelper
.
resizeCoordinatesToImageElement
(
options
.
imageEl
,
this
.
actual
);
}
}
}
app/assets/javascripts/image_diff/image_diff.js
0 → 100644
View file @
b54203f0
import
imageDiffHelper
from
'
./helpers/index
'
;
import
ImageBadge
from
'
./image_badge
'
;
import
{
isImageLoaded
}
from
'
../lib/utils/image_utility
'
;
export
default
class
ImageDiff
{
constructor
(
el
,
options
)
{
this
.
el
=
el
;
this
.
canCreateNote
=
!!
(
options
&&
options
.
canCreateNote
);
this
.
renderCommentBadge
=
!!
(
options
&&
options
.
renderCommentBadge
);
this
.
$noteContainer
=
$
(
'
.note-container
'
,
this
.
el
);
this
.
imageBadges
=
[];
}
init
()
{
this
.
imageFrameEl
=
this
.
el
.
querySelector
(
'
.diff-file .js-image-frame
'
);
this
.
imageEl
=
this
.
imageFrameEl
.
querySelector
(
'
img
'
);
this
.
bindEvents
();
}
bindEvents
()
{
this
.
imageClickedWrapper
=
this
.
imageClicked
.
bind
(
this
);
this
.
imageBlurredWrapper
=
imageDiffHelper
.
removeCommentIndicator
.
bind
(
null
,
this
.
imageFrameEl
);
this
.
addBadgeWrapper
=
this
.
addBadge
.
bind
(
this
);
this
.
removeBadgeWrapper
=
this
.
removeBadge
.
bind
(
this
);
this
.
renderBadgesWrapper
=
this
.
renderBadges
.
bind
(
this
);
// Render badges
if
(
isImageLoaded
(
this
.
imageEl
))
{
this
.
renderBadges
();
}
else
{
this
.
imageEl
.
addEventListener
(
'
load
'
,
this
.
renderBadgesWrapper
);
}
// jquery makes the event delegation here much simpler
this
.
$noteContainer
.
on
(
'
click
'
,
'
.js-diff-notes-toggle
'
,
imageDiffHelper
.
toggleCollapsed
);
$
(
this
.
el
).
on
(
'
click
'
,
'
.comment-indicator
'
,
imageDiffHelper
.
commentIndicatorOnClick
);
if
(
this
.
canCreateNote
)
{
this
.
el
.
addEventListener
(
'
click.imageDiff
'
,
this
.
imageClickedWrapper
);
this
.
el
.
addEventListener
(
'
blur.imageDiff
'
,
this
.
imageBlurredWrapper
);
this
.
el
.
addEventListener
(
'
addBadge.imageDiff
'
,
this
.
addBadgeWrapper
);
this
.
el
.
addEventListener
(
'
removeBadge.imageDiff
'
,
this
.
removeBadgeWrapper
);
}
}
imageClicked
(
event
)
{
const
customEvent
=
event
.
detail
;
const
selection
=
imageDiffHelper
.
getTargetSelection
(
customEvent
);
const
el
=
customEvent
.
currentTarget
;
imageDiffHelper
.
setPositionDataAttribute
(
el
,
selection
.
actual
);
imageDiffHelper
.
showCommentIndicator
(
this
.
imageFrameEl
,
selection
.
browser
);
}
renderBadges
()
{
const
discussionsEls
=
this
.
el
.
querySelectorAll
(
'
.note-container .discussion-notes .notes
'
);
[...
discussionsEls
].
forEach
(
this
.
renderBadge
.
bind
(
this
));
}
renderBadge
(
discussionEl
,
index
)
{
const
imageBadge
=
imageDiffHelper
.
generateBadgeFromDiscussionDOM
(
this
.
imageFrameEl
,
discussionEl
);
this
.
imageBadges
.
push
(
imageBadge
);
const
options
=
{
coordinate
:
imageBadge
.
browser
,
noteId
:
imageBadge
.
noteId
,
};
if
(
this
.
renderCommentBadge
)
{
imageDiffHelper
.
addImageCommentBadge
(
this
.
imageFrameEl
,
options
);
}
else
{
const
numberBadgeOptions
=
Object
.
assign
({},
options
,
{
badgeText
:
index
+
1
,
});
imageDiffHelper
.
addImageBadge
(
this
.
imageFrameEl
,
numberBadgeOptions
);
}
}
addBadge
(
event
)
{
const
{
x
,
y
,
width
,
height
,
noteId
,
discussionId
}
=
event
.
detail
;
const
badgeText
=
this
.
imageBadges
.
length
+
1
;
const
imageBadge
=
new
ImageBadge
({
actual
:
{
x
,
y
,
width
,
height
,
},
imageEl
:
this
.
imageFrameEl
.
querySelector
(
'
img
'
),
noteId
,
discussionId
,
});
this
.
imageBadges
.
push
(
imageBadge
);
imageDiffHelper
.
addImageBadge
(
this
.
imageFrameEl
,
{
coordinate
:
imageBadge
.
browser
,
badgeText
,
noteId
,
});
imageDiffHelper
.
addAvatarBadge
(
this
.
el
,
{
detail
:
{
noteId
,
badgeNumber
:
badgeText
,
},
});
const
discussionEl
=
this
.
el
.
querySelector
(
`#discussion_
${
discussionId
}
`
);
imageDiffHelper
.
updateDiscussionBadgeNumber
(
discussionEl
,
badgeText
);
}
removeBadge
(
event
)
{
const
{
badgeNumber
}
=
event
.
detail
;
const
indexToRemove
=
badgeNumber
-
1
;
const
imageBadgeEls
=
this
.
imageFrameEl
.
querySelectorAll
(
'
.badge
'
);
if
(
this
.
imageBadges
.
length
!==
badgeNumber
)
{
// Cascade badges count numbers for (avatar badges + image badges)
this
.
imageBadges
.
forEach
((
badge
,
index
)
=>
{
if
(
index
>
indexToRemove
)
{
const
{
discussionId
}
=
badge
;
const
updatedBadgeNumber
=
index
;
const
discussionEl
=
this
.
el
.
querySelector
(
`#discussion_
${
discussionId
}
`
);
imageBadgeEls
[
index
].
innerText
=
updatedBadgeNumber
;
imageDiffHelper
.
updateDiscussionBadgeNumber
(
discussionEl
,
updatedBadgeNumber
);
imageDiffHelper
.
updateDiscussionAvatarBadgeNumber
(
discussionEl
,
updatedBadgeNumber
);
}
});
}
this
.
imageBadges
.
splice
(
indexToRemove
,
1
);
const
imageBadgeEl
=
imageBadgeEls
[
indexToRemove
];
imageBadgeEl
.
remove
();
}
}
app/assets/javascripts/image_diff/init_discussion_tab.js
0 → 100644
View file @
b54203f0
import
imageDiffHelper
from
'
./helpers/index
'
;
export
default
()
=>
{
// Always pass can-create-note as false because a user
// cannot place new badge markers on discussion tab
const
canCreateNote
=
false
;
const
renderCommentBadge
=
true
;
const
diffFileEls
=
document
.
querySelectorAll
(
'
.timeline-content .diff-file.js-image-file
'
);
[...
diffFileEls
].
forEach
(
diffFileEl
=>
imageDiffHelper
.
initImageDiff
(
diffFileEl
,
canCreateNote
,
renderCommentBadge
));
};
app/assets/javascripts/image_diff/replaced_image_diff.js
0 → 100644
View file @
b54203f0
import
imageDiffHelper
from
'
./helpers/index
'
;
import
{
viewTypes
,
isValidViewType
}
from
'
./view_types
'
;
import
ImageDiff
from
'
./image_diff
'
;
export
default
class
ReplacedImageDiff
extends
ImageDiff
{
init
(
defaultViewType
=
viewTypes
.
TWO_UP
)
{
this
.
imageFrameEls
=
{
[
viewTypes
.
TWO_UP
]:
this
.
el
.
querySelector
(
'
.two-up .js-image-frame
'
),
[
viewTypes
.
SWIPE
]:
this
.
el
.
querySelector
(
'
.swipe .js-image-frame
'
),
[
viewTypes
.
ONION_SKIN
]:
this
.
el
.
querySelector
(
'
.onion-skin .js-image-frame
'
),
};
const
viewModesEl
=
this
.
el
.
querySelector
(
'
.view-modes-menu
'
);
this
.
viewModesEls
=
{
[
viewTypes
.
TWO_UP
]:
viewModesEl
.
querySelector
(
'
.two-up
'
),
[
viewTypes
.
SWIPE
]:
viewModesEl
.
querySelector
(
'
.swipe
'
),
[
viewTypes
.
ONION_SKIN
]:
viewModesEl
.
querySelector
(
'
.onion-skin
'
),
};
this
.
currentView
=
defaultViewType
;
this
.
generateImageEls
();
this
.
bindEvents
();
}
generateImageEls
()
{
this
.
imageEls
=
{};
const
viewTypeNames
=
Object
.
getOwnPropertyNames
(
viewTypes
);
viewTypeNames
.
forEach
((
viewType
)
=>
{
this
.
imageEls
[
viewType
]
=
this
.
imageFrameEls
[
viewType
].
querySelector
(
'
img
'
);
});
}
bindEvents
()
{
super
.
bindEvents
();
this
.
changeToViewTwoUp
=
this
.
changeView
.
bind
(
this
,
viewTypes
.
TWO_UP
);
this
.
changeToViewSwipe
=
this
.
changeView
.
bind
(
this
,
viewTypes
.
SWIPE
);
this
.
changeToViewOnionSkin
=
this
.
changeView
.
bind
(
this
,
viewTypes
.
ONION_SKIN
);
this
.
viewModesEls
[
viewTypes
.
TWO_UP
].
addEventListener
(
'
click
'
,
this
.
changeToViewTwoUp
);
this
.
viewModesEls
[
viewTypes
.
SWIPE
].
addEventListener
(
'
click
'
,
this
.
changeToViewSwipe
);
this
.
viewModesEls
[
viewTypes
.
ONION_SKIN
].
addEventListener
(
'
click
'
,
this
.
changeToViewOnionSkin
);
}
get
imageEl
()
{
return
this
.
imageEls
[
this
.
currentView
];
}
get
imageFrameEl
()
{
return
this
.
imageFrameEls
[
this
.
currentView
];
}
changeView
(
newView
)
{
if
(
!
isValidViewType
(
newView
))
{
return
;
}
const
indicator
=
imageDiffHelper
.
removeCommentIndicator
(
this
.
imageFrameEl
);
this
.
currentView
=
newView
;
// Clear existing badges on new view
const
existingBadges
=
this
.
imageFrameEl
.
querySelectorAll
(
'
.badge
'
);
[...
existingBadges
].
map
(
badge
=>
badge
.
remove
());
// Remove existing references to old view image badges
this
.
imageBadges
=
[];
// Image_file.js has a fade animation of 200ms for loading the view
// Need to wait an additional 250ms for the images to be displayed
// on window in order to re-normalize their dimensions
setTimeout
(
this
.
renderNewView
.
bind
(
this
,
indicator
),
250
);
}
renderNewView
(
indicator
)
{
// Generate badge coordinates on new view
this
.
renderBadges
();
// Re-render indicator in new view
if
(
indicator
.
removed
)
{
const
normalizedIndicator
=
imageDiffHelper
.
resizeCoordinatesToImageElement
(
this
.
imageEl
,
{
x
:
indicator
.
x
,
y
:
indicator
.
y
,
width
:
indicator
.
image
.
width
,
height
:
indicator
.
image
.
height
,
});
imageDiffHelper
.
showCommentIndicator
(
this
.
imageFrameEl
,
normalizedIndicator
);
}
}
}
app/assets/javascripts/image_diff/view_types.js
0 → 100644
View file @
b54203f0
export
const
viewTypes
=
{
TWO_UP
:
'
TWO_UP
'
,
SWIPE
:
'
SWIPE
'
,
ONION_SKIN
:
'
ONION_SKIN
'
,
};
export
function
isValidViewType
(
validate
)
{
return
!!
Object
.
getOwnPropertyNames
(
viewTypes
).
find
(
viewType
=>
viewType
===
validate
);
}
app/assets/javascripts/lib/utils/image_utility.js
0 → 100644
View file @
b54203f0
/* eslint-disable import/prefer-default-export */
export
function
isImageLoaded
(
element
)
{
return
element
.
complete
&&
element
.
naturalHeight
!==
0
;
}
app/assets/javascripts/main.js
View file @
b54203f0
...
@@ -35,8 +35,6 @@ import './shortcuts_network';
...
@@ -35,8 +35,6 @@ import './shortcuts_network';
import
'
./templates/issuable_template_selector
'
;
import
'
./templates/issuable_template_selector
'
;
import
'
./templates/issuable_template_selectors
'
;
import
'
./templates/issuable_template_selectors
'
;
// commit
import
'
./commit/file
'
;
import
'
./commit/image_file
'
;
import
'
./commit/image_file
'
;
// lib/utils
// lib/utils
...
@@ -70,7 +68,6 @@ import './build';
...
@@ -70,7 +68,6 @@ import './build';
import
'
./build_artifacts
'
;
import
'
./build_artifacts
'
;
import
'
./build_variables
'
;
import
'
./build_variables
'
;
import
'
./ci_lint_editor
'
;
import
'
./ci_lint_editor
'
;
import
'
./commit
'
;
import
'
./commits
'
;
import
'
./commits
'
;
import
'
./compare
'
;
import
'
./compare
'
;
import
'
./compare_autocomplete
'
;
import
'
./compare_autocomplete
'
;
...
...
app/assets/javascripts/merge_request_tabs.js
View file @
b54203f0
...
@@ -13,6 +13,8 @@ import {
...
@@ -13,6 +13,8 @@ import {
isMetaClick
,
isMetaClick
,
}
from
'
./lib/utils/common_utils
'
;
}
from
'
./lib/utils/common_utils
'
;
import
initDiscussionTab
from
'
./image_diff/init_discussion_tab
'
;
/* eslint-disable max-len */
/* eslint-disable max-len */
// MergeRequestTabs
// MergeRequestTabs
//
//
...
@@ -154,6 +156,8 @@ import {
...
@@ -154,6 +156,8 @@ import {
}
}
this
.
resetViewContainer
();
this
.
resetViewContainer
();
this
.
destroyPipelinesView
();
this
.
destroyPipelinesView
();
initDiscussionTab
();
}
}
if
(
this
.
setUrl
)
{
if
(
this
.
setUrl
)
{
this
.
setCurrentAction
(
action
);
this
.
setCurrentAction
(
action
);
...
...
app/assets/javascripts/notes.js
View file @
b54203f0
...
@@ -24,6 +24,7 @@ import './autosave';
...
@@ -24,6 +24,7 @@ import './autosave';
import
'
./dropzone_input
'
;
import
'
./dropzone_input
'
;
import
TaskList
from
'
./task_list
'
;
import
TaskList
from
'
./task_list
'
;
import
{
ajaxPost
,
isInViewport
,
getPagePath
,
scrollToElement
,
isMetaKey
}
from
'
./lib/utils/common_utils
'
;
import
{
ajaxPost
,
isInViewport
,
getPagePath
,
scrollToElement
,
isMetaKey
}
from
'
./lib/utils/common_utils
'
;
import
imageDiffHelper
from
'
./image_diff/helpers/index
'
;
window
.
autosize
=
autosize
;
window
.
autosize
=
autosize
;
window
.
Dropzone
=
Dropzone
;
window
.
Dropzone
=
Dropzone
;
...
@@ -42,6 +43,7 @@ export default class Notes {
...
@@ -42,6 +43,7 @@ export default class Notes {
this
.
visibilityChange
=
this
.
visibilityChange
.
bind
(
this
);
this
.
visibilityChange
=
this
.
visibilityChange
.
bind
(
this
);
this
.
cancelDiscussionForm
=
this
.
cancelDiscussionForm
.
bind
(
this
);
this
.
cancelDiscussionForm
=
this
.
cancelDiscussionForm
.
bind
(
this
);
this
.
onAddDiffNote
=
this
.
onAddDiffNote
.
bind
(
this
);
this
.
onAddDiffNote
=
this
.
onAddDiffNote
.
bind
(
this
);
this
.
onAddImageDiffNote
=
this
.
onAddImageDiffNote
.
bind
(
this
);
this
.
setupDiscussionNoteForm
=
this
.
setupDiscussionNoteForm
.
bind
(
this
);
this
.
setupDiscussionNoteForm
=
this
.
setupDiscussionNoteForm
.
bind
(
this
);
this
.
onReplyToDiscussionNote
=
this
.
onReplyToDiscussionNote
.
bind
(
this
);
this
.
onReplyToDiscussionNote
=
this
.
onReplyToDiscussionNote
.
bind
(
this
);
this
.
removeNote
=
this
.
removeNote
.
bind
(
this
);
this
.
removeNote
=
this
.
removeNote
.
bind
(
this
);
...
@@ -114,6 +116,8 @@ export default class Notes {
...
@@ -114,6 +116,8 @@ export default class Notes {
$
(
document
).
on
(
'
click
'
,
'
.js-discussion-reply-button
'
,
this
.
onReplyToDiscussionNote
);
$
(
document
).
on
(
'
click
'
,
'
.js-discussion-reply-button
'
,
this
.
onReplyToDiscussionNote
);
// add diff note
// add diff note
$
(
document
).
on
(
'
click
'
,
'
.js-add-diff-note-button
'
,
this
.
onAddDiffNote
);
$
(
document
).
on
(
'
click
'
,
'
.js-add-diff-note-button
'
,
this
.
onAddDiffNote
);
// add diff note for images
$
(
document
).
on
(
'
click
'
,
'
.js-add-image-diff-note-button
'
,
this
.
onAddImageDiffNote
);
// hide diff note form
// hide diff note form
$
(
document
).
on
(
'
click
'
,
'
.js-close-discussion-note-form
'
,
this
.
cancelDiscussionForm
);
$
(
document
).
on
(
'
click
'
,
'
.js-close-discussion-note-form
'
,
this
.
cancelDiscussionForm
);
// toggle commit list
// toggle commit list
...
@@ -140,6 +144,7 @@ export default class Notes {
...
@@ -140,6 +144,7 @@ export default class Notes {
$
(
document
).
off
(
'
click
'
,
'
.js-note-attachment-delete
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-attachment-delete
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-discussion-reply-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-discussion-reply-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-add-diff-note-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-add-diff-note-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-add-image-diff-note-button
'
);
$
(
document
).
off
(
'
visibilitychange
'
);
$
(
document
).
off
(
'
visibilitychange
'
);
$
(
document
).
off
(
'
keyup input
'
,
'
.js-note-text
'
);
$
(
document
).
off
(
'
keyup input
'
,
'
.js-note-text
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-target-reopen
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-target-reopen
'
);
...
@@ -412,6 +417,11 @@ export default class Notes {
...
@@ -412,6 +417,11 @@ export default class Notes {
this
.
note_ids
.
push
(
noteEntity
.
id
);
this
.
note_ids
.
push
(
noteEntity
.
id
);
form
=
$form
||
$
(
`.js-discussion-note-form[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
);
form
=
$form
||
$
(
`.js-discussion-note-form[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
);
row
=
form
.
closest
(
'
tr
'
);
row
=
form
.
closest
(
'
tr
'
);
if
(
noteEntity
.
on_image
)
{
row
=
form
;
}
lineType
=
this
.
isParallelView
()
?
form
.
find
(
'
#line_type
'
).
val
()
:
'
old
'
;
lineType
=
this
.
isParallelView
()
?
form
.
find
(
'
#line_type
'
).
val
()
:
'
old
'
;
diffAvatarContainer
=
row
.
prevAll
(
'
.line_holder
'
).
first
().
find
(
'
.js-avatar-container.
'
+
lineType
+
'
_line
'
);
diffAvatarContainer
=
row
.
prevAll
(
'
.line_holder
'
).
first
().
find
(
'
.js-avatar-container.
'
+
lineType
+
'
_line
'
);
// is this the first note of discussion?
// is this the first note of discussion?
...
@@ -423,7 +433,7 @@ export default class Notes {
...
@@ -423,7 +433,7 @@ export default class Notes {
if
(
noteEntity
.
diff_discussion_html
)
{
if
(
noteEntity
.
diff_discussion_html
)
{
var
$discussion
=
$
(
noteEntity
.
diff_discussion_html
).
renderGFM
();
var
$discussion
=
$
(
noteEntity
.
diff_discussion_html
).
renderGFM
();
if
(
!
this
.
isParallelView
()
||
row
.
hasClass
(
'
js-temp-notes-holder
'
))
{
if
(
!
this
.
isParallelView
()
||
row
.
hasClass
(
'
js-temp-notes-holder
'
)
||
noteEntity
.
on_image
)
{
// insert the note and the reply button after the temp row
// insert the note and the reply button after the temp row
row
.
after
(
$discussion
);
row
.
after
(
$discussion
);
}
else
{
}
else
{
...
@@ -449,6 +459,7 @@ export default class Notes {
...
@@ -449,6 +459,7 @@ export default class Notes {
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
&&
noteEntity
.
discussion_resolvable
)
{
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
&&
noteEntity
.
discussion_resolvable
)
{
gl
.
diffNotesCompileComponents
();
gl
.
diffNotesCompileComponents
();
this
.
renderDiscussionAvatar
(
diffAvatarContainer
,
noteEntity
);
this
.
renderDiscussionAvatar
(
diffAvatarContainer
,
noteEntity
);
}
}
...
@@ -561,7 +572,7 @@ export default class Notes {
...
@@ -561,7 +572,7 @@ export default class Notes {
form
.
find
(
'
#note_line_code
'
).
val
(),
form
.
find
(
'
#note_line_code
'
).
val
(),
// DiffNote
// DiffNote
form
.
find
(
'
#note_position
'
).
val
()
form
.
find
(
'
#note_position
'
).
val
()
,
];
];
return
new
Autosave
(
textarea
,
key
);
return
new
Autosave
(
textarea
,
key
);
}
}
...
@@ -783,9 +794,22 @@ export default class Notes {
...
@@ -783,9 +794,22 @@ export default class Notes {
$
(
`.js-diff-avatars-
${
discussionId
}
`
).
trigger
(
'
remove.vue
'
);
$
(
`.js-diff-avatars-
${
discussionId
}
`
).
trigger
(
'
remove.vue
'
);
// The notes tr can contain multiple lists of notes, like on the parallel diff
// The notes tr can contain multiple lists of notes, like on the parallel diff
if
(
notesTr
.
find
(
'
.discussion-notes
'
).
length
>
1
)
{
// notesTr does not exist for image diffs
if
(
notesTr
.
find
(
'
.discussion-notes
'
).
length
>
1
||
notesTr
.
length
===
0
)
{
const
$diffFile
=
$notes
.
closest
(
'
.diff-file
'
);
if
(
$diffFile
.
length
>
0
)
{
const
removeBadgeEvent
=
new
CustomEvent
(
'
removeBadge.imageDiff
'
,
{
detail
:
{
// badgeNumber's start with 1 and index starts with 0
badgeNumber
:
$notes
.
index
()
+
1
,
},
});
$diffFile
[
0
].
dispatchEvent
(
removeBadgeEvent
);
}
$notes
.
remove
();
$notes
.
remove
();
}
else
{
}
else
if
(
notesTr
.
length
>
0
)
{
notesTr
.
remove
();
notesTr
.
remove
();
}
}
}
}
...
@@ -841,7 +865,11 @@ export default class Notes {
...
@@ -841,7 +865,11 @@ export default class Notes {
*/
*/
setupDiscussionNoteForm
(
dataHolder
,
form
)
{
setupDiscussionNoteForm
(
dataHolder
,
form
)
{
// setup note target
// setup note target
const
diffFileData
=
dataHolder
.
closest
(
'
.text-file
'
);
let
diffFileData
=
dataHolder
.
closest
(
'
.text-file
'
);
if
(
diffFileData
.
length
===
0
)
{
diffFileData
=
dataHolder
.
closest
(
'
.image
'
);
}
var
discussionID
=
dataHolder
.
data
(
'
discussionId
'
);
var
discussionID
=
dataHolder
.
data
(
'
discussionId
'
);
...
@@ -907,6 +935,31 @@ export default class Notes {
...
@@ -907,6 +935,31 @@ export default class Notes {
});
});
}
}
onAddImageDiffNote
(
e
)
{
const
$link
=
$
(
e
.
currentTarget
||
e
.
target
);
const
$diffFile
=
$link
.
closest
(
'
.diff-file
'
);
const
clickEvent
=
new
CustomEvent
(
'
click.imageDiff
'
,
{
detail
:
e
,
});
$diffFile
[
0
].
dispatchEvent
(
clickEvent
);
// Setup comment form
let
newForm
;
const
$noteContainer
=
$link
.
closest
(
'
.diff-viewer
'
).
find
(
'
.note-container
'
);
const
$form
=
$noteContainer
.
find
(
'
> .discussion-form
'
);
if
(
$form
.
length
===
0
)
{
newForm
=
this
.
cleanForm
(
this
.
formClone
.
clone
());
newForm
.
appendTo
(
$noteContainer
);
}
else
{
newForm
=
$form
;
}
this
.
setupDiscussionNoteForm
(
$link
,
newForm
);
}
toggleDiffNote
({
toggleDiffNote
({
target
,
target
,
lineType
,
lineType
,
...
@@ -999,10 +1052,25 @@ export default class Notes {
...
@@ -999,10 +1052,25 @@ export default class Notes {
}
}
cancelDiscussionForm
(
e
)
{
cancelDiscussionForm
(
e
)
{
var
form
;
e
.
preventDefault
();
e
.
preventDefault
();
form
=
$
(
e
.
target
).
closest
(
'
.js-discussion-note-form
'
);
const
$form
=
$
(
e
.
target
).
closest
(
'
.js-discussion-note-form
'
);
return
this
.
removeDiscussionNoteForm
(
form
);
const
$discussionNote
=
$
(
e
.
target
).
closest
(
'
.discussion-notes
'
);
if
(
$discussionNote
.
length
===
0
)
{
// Only send blur event when the discussion form
// is not part of a discussion note
const
$diffFile
=
$form
.
closest
(
'
.diff-file
'
);
if
(
$diffFile
.
length
>
0
)
{
const
blurEvent
=
new
CustomEvent
(
'
blur.imageDiff
'
,
{
detail
:
e
,
});
$diffFile
[
0
].
dispatchEvent
(
blurEvent
);
}
}
return
this
.
removeDiscussionNoteForm
(
$form
);
}
}
/**
/**
...
@@ -1414,6 +1482,15 @@ export default class Notes {
...
@@ -1414,6 +1482,15 @@ export default class Notes {
// Submission successful! remove placeholder
// Submission successful! remove placeholder
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
const
$diffFile
=
$form
.
closest
(
'
.diff-file
'
);
if
(
$diffFile
.
length
>
0
)
{
const
blurEvent
=
new
CustomEvent
(
'
blur.imageDiff
'
,
{
detail
:
e
,
});
$diffFile
[
0
].
dispatchEvent
(
blurEvent
);
}
// Reset cached commands list when command is applied
// Reset cached commands list when command is applied
if
(
hasQuickActions
)
{
if
(
hasQuickActions
)
{
$form
.
find
(
'
textarea.js-note-text
'
).
trigger
(
'
clear-commands-cache.atwho
'
);
$form
.
find
(
'
textarea.js-note-text
'
).
trigger
(
'
clear-commands-cache.atwho
'
);
...
@@ -1436,7 +1513,28 @@ export default class Notes {
...
@@ -1436,7 +1513,28 @@ export default class Notes {
}
}
// Show final note element on UI
// Show final note element on UI
this
.
addDiscussionNote
(
$form
,
note
,
$notesContainer
.
length
===
0
);
const
isNewDiffComment
=
$notesContainer
.
length
===
0
;
this
.
addDiscussionNote
(
$form
,
note
,
isNewDiffComment
);
if
(
isNewDiffComment
)
{
// Add image badge, avatar badge and toggle discussion badge for new image diffs
const
notePosition
=
$form
.
find
(
'
#note_position
'
).
val
();
if
(
$diffFile
.
length
>
0
&&
notePosition
.
length
>
0
)
{
const
{
x
,
y
,
width
,
height
}
=
JSON
.
parse
(
notePosition
);
const
addBadgeEvent
=
new
CustomEvent
(
'
addBadge.imageDiff
'
,
{
detail
:
{
x
,
y
,
width
,
height
,
noteId
:
`note_
${
note
.
id
}
`
,
discussionId
:
note
.
discussion_id
,
},
});
$diffFile
[
0
].
dispatchEvent
(
addBadgeEvent
);
}
}
// append flash-container to the Notes list
// append flash-container to the Notes list
if
(
$notesContainer
.
length
)
{
if
(
$notesContainer
.
length
)
{
...
@@ -1457,6 +1555,16 @@ export default class Notes {
...
@@ -1457,6 +1555,16 @@ export default class Notes {
// Submission failed, remove placeholder note and show Flash error message
// Submission failed, remove placeholder note and show Flash error message
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
const
blurEvent
=
new
CustomEvent
(
'
blur.imageDiff
'
,
{
detail
:
e
,
});
const
closestDiffFile
=
$form
.
closest
(
'
.diff-file
'
);
if
(
closestDiffFile
.
length
)
{
closestDiffFile
[
0
].
dispatchEvent
(
blurEvent
);
}
if
(
hasQuickActions
)
{
if
(
hasQuickActions
)
{
$notesContainer
.
find
(
`#
${
systemNoteUniqueId
}
`
).
remove
();
$notesContainer
.
find
(
`#
${
systemNoteUniqueId
}
`
).
remove
();
}
}
...
@@ -1500,6 +1608,8 @@ export default class Notes {
...
@@ -1500,6 +1608,8 @@ export default class Notes {
const
$noteBody
=
$editingNote
.
find
(
'
.js-task-list-container
'
);
const
$noteBody
=
$editingNote
.
find
(
'
.js-task-list-container
'
);
const
$noteBodyText
=
$noteBody
.
find
(
'
.note-text
'
);
const
$noteBodyText
=
$noteBody
.
find
(
'
.note-text
'
);
const
{
formData
,
formContent
,
formAction
}
=
this
.
getFormData
(
$form
);
const
{
formData
,
formContent
,
formAction
}
=
this
.
getFormData
(
$form
);
const
$diffFile
=
$form
.
closest
(
'
.diff-file
'
);
const
$notesContainer
=
$form
.
closest
(
'
.notes
'
);
// Cache original comment content
// Cache original comment content
const
cachedNoteBodyText
=
$noteBodyText
.
html
();
const
cachedNoteBodyText
=
$noteBodyText
.
html
();
...
...
app/assets/javascripts/single_file_diff.js
View file @
b54203f0
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import
FilesCommentButton
from
'
./files_comment_button
'
;
import
FilesCommentButton
from
'
./files_comment_button
'
;
import
imageDiffHelper
from
'
./image_diff/helpers/index
'
;
const
WRAPPER
=
'
<div class="diff-content"></div>
'
;
const
WRAPPER
=
'
<div class="diff-content"></div>
'
;
const
LOADING_HTML
=
'
<i class="fa fa-spinner fa-spin"></i>
'
;
const
LOADING_HTML
=
'
<i class="fa fa-spinner fa-spin"></i>
'
;
...
@@ -74,7 +75,11 @@ export default class SingleFileDiff {
...
@@ -74,7 +75,11 @@ export default class SingleFileDiff {
gl
.
diffNotesCompileComponents
();
gl
.
diffNotesCompileComponents
();
}
}
FilesCommentButton
.
init
(
$
(
_this
.
file
));
const
$file
=
$
(
_this
.
file
);
FilesCommentButton
.
init
(
$file
);
const
canCreateNote
=
$file
.
closest
(
'
.files
'
).
is
(
'
[data-can-create-note]
'
);
imageDiffHelper
.
initImageDiff
(
$file
[
0
],
canCreateNote
);
if
(
cb
)
cb
();
if
(
cb
)
cb
();
};
};
...
...
app/assets/stylesheets/framework/buttons.scss
View file @
b54203f0
@mixin
btn-comment-icon
{
border-radius
:
50%
;
background
:
$white-light
;
padding
:
1px
5px
;
font-size
:
12px
;
color
:
$blue-500
;
width
:
23px
;
height
:
23px
;
border
:
1px
solid
$blue-500
;
&
:hover
,
&
.inverted
{
background
:
$blue-500
;
border-color
:
$blue-600
;
color
:
$white-light
;
}
&
:active
{
outline
:
0
;
}
}
@mixin
btn-default
{
@mixin
btn-default
{
border-radius
:
3px
;
border-radius
:
3px
;
font-size
:
$gl-font-size
;
font-size
:
$gl-font-size
;
...
...
app/assets/stylesheets/framework/timeline.scss
View file @
b54203f0
...
@@ -17,15 +17,19 @@
...
@@ -17,15 +17,19 @@
.diff-file
{
.diff-file
{
border
:
1px
solid
$border-color
;
border
:
1px
solid
$border-color
;
border-bottom
:
none
;
margin
:
0
;
margin
:
0
;
}
}
&
.text-file
.diff-file
{
border-bottom
:
none
;
}
}
}
.timeline-entry
{
.timeline-entry
{
border-color
:
$white-normal
;
border-color
:
$white-normal
;
color
:
$gl-text-color
;
color
:
$gl-text-color
;
border-bottom
:
1px
solid
$border-white-light
;
border-bottom
:
1px
solid
$border-white-light
;
background
:
$white-light
;
.timeline-entry-inner
{
.timeline-entry-inner
{
position
:
relative
;
position
:
relative
;
...
...
app/assets/stylesheets/framework/variables.scss
View file @
b54203f0
...
@@ -323,6 +323,7 @@ $diff-image-info-color: grey;
...
@@ -323,6 +323,7 @@ $diff-image-info-color: grey;
$diff-swipe-border
:
#999
;
$diff-swipe-border
:
#999
;
$diff-view-modes-color
:
grey
;
$diff-view-modes-color
:
grey
;
$diff-view-modes-border
:
#c1c1c1
;
$diff-view-modes-border
:
#c1c1c1
;
$diff-jagged-border-gradient-color
:
darken
(
$white-normal
,
8%
);
/*
/*
* Fonts
* Fonts
...
@@ -712,3 +713,9 @@ Issuable warning
...
@@ -712,3 +713,9 @@ Issuable warning
*/
*/
$issuable-warning-size
:
24px
;
$issuable-warning-size
:
24px
;
$issuable-warning-icon-margin
:
4px
;
$issuable-warning-icon-margin
:
4px
;
/*
Image Commenting cursor
*/
$image-comment-cursor-left-offset
:
12
;
$image-comment-cursor-top-offset
:
30
;
app/assets/stylesheets/pages/diff.scss
View file @
b54203f0
...
@@ -297,6 +297,7 @@
...
@@ -297,6 +297,7 @@
.drag-track
{
.drag-track
{
display
:
block
;
display
:
block
;
position
:
absolute
;
position
:
absolute
;
top
:
0
;
left
:
12px
;
left
:
12px
;
height
:
10px
;
height
:
10px
;
width
:
276px
;
width
:
276px
;
...
@@ -547,16 +548,23 @@
...
@@ -547,16 +548,23 @@
}
}
.diff-notes-collapse
{
.diff-notes-collapse
{
width
:
19px
;
width
:
24px
;
height
:
19px
;
height
:
24px
;
border-radius
:
50%
;
padding
:
0
;
padding
:
0
;
transition
:
transform
.1s
ease-out
;
transition
:
transform
.1s
ease-out
;
z-index
:
100
;
z-index
:
100
;
.collapse-icon
{
height
:
50%
;
width
:
100%
;
}
svg
{
svg
{
vertical-align
:
text-top
;
vertical-align
:
middle
;
}
}
.collapse-icon
,
path
{
path
{
fill
:
$white-light
;
fill
:
$white-light
;
}
}
...
@@ -644,3 +652,157 @@
...
@@ -644,3 +652,157 @@
text-overflow
:
ellipsis
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
white-space
:
nowrap
;
}
}
.note-container
{
background-color
:
$gray-light
;
border-top
:
1px
solid
$white-normal
;
// double jagged line divider
.
discussion-notes
+ .
discussion-notes
:
:
before
,
.
discussion-notes
+
.
discussion-form
::
before
{
content
:
''
;
position
:
relative
;
display
:
block
;
width
:
100%
;
height
:
10px
;
background-color
:
$white-light
;
background-image
:
linear-gradient
(
45deg
,
transparent
,
transparent
73%
,
$diff-jagged-border-gradient-color
75%
,
$white-light
80%
)
,
linear-gradient
(
225deg
,
transparent
,
transparent
73%
,
$diff-jagged-border-gradient-color
75%
,
$white-light
80%
)
,
linear-gradient
(
135deg
,
transparent
,
transparent
73%
,
$diff-jagged-border-gradient-color
75%
,
$white-light
80%
)
,
linear-gradient
(
-45deg
,
transparent
,
transparent
73%
,
$diff-jagged-border-gradient-color
75%
,
$white-light
80%
);
background-position
:
5px
5px
,
0
5px
,
0
5px
,
5px
5px
;
background-size
:
10px
10px
;
background-repeat
:
repeat
;
}
.notes
{
position
:
relative
;
}
.diff-notes-collapse
{
position
:
absolute
;
left
:
-12px
;
}
}
.diff-file
.note-container
>
.new-note
,
.note-container
.discussion-notes
{
margin-left
:
100px
;
border-left
:
1px
solid
$white-normal
;
}
.notes.active
{
.diff-file
.note-container
>
.new-note
,
.note-container
.discussion-notes
{
// Override our margin and border (set for diff tab)
// when user is on the discussion tab for MR
margin-left
:
inherit
;
border-left
:
inherit
;
}
}
.files
:not
([
data-can-create-note
])
.frame
{
cursor
:
auto
;
}
.frame.click-to-comment
{
position
:
relative
;
cursor
:
url(icon_image_comment.svg)
$image-comment-cursor-left-offset
$image-comment-cursor-top-offset
,
auto
;
// Retina cursor
cursor
:
-webkit-image-set
(
url(icon_image_comment.svg)
1x
,
url(icon_image_comment@2x.svg)
2x
)
$image-comment-cursor-left-offset
$image-comment-cursor-top-offset
,
auto
;
.comment-indicator
{
position
:
absolute
;
padding
:
0
;
width
:
(
2px
*
$image-comment-cursor-left-offset
);
height
:
(
1px
*
$image-comment-cursor-top-offset
);
// center the indicator to match the top left click region
margin-top
:
(
-1px
*
$image-comment-cursor-top-offset
)
+
2
;
margin-left
:
(
-1px
*
$image-comment-cursor-left-offset
)
+
1
;
svg
{
width
:
100%
;
height
:
100%
;
}
&
:focus
{
outline
:
none
;
}
}
}
.frame
.badge
,
.image-diff-avatar-link
.badge
,
.notes
>
.badge
{
position
:
absolute
;
background-color
:
$blue-400
;
color
:
$white-light
;
border
:
$white-light
1px
solid
;
min-height
:
$gl-padding
;
padding
:
5px
8px
;
border-radius
:
12px
;
&
:focus
{
outline
:
none
;
}
}
.frame
.badge
,
.frame
.image-comment-badge
{
// Center align badges on the frame
transform
:
translate3d
(
-50%
,
-50%
,
0
);
}
.image-comment-badge
{
@include
btn-comment-icon
;
position
:
absolute
;
&
.inverted
{
border-color
:
$white-light
;
}
}
.image-diff-avatar-link
{
position
:
relative
;
.badge
,
.image-comment-badge
{
top
:
25px
;
right
:
8px
;
}
}
.notes
>
.badge
{
display
:
none
;
left
:
-13px
;
}
.discussion-notes
{
min-height
:
35px
;
&
:first-child
{
// First child does not have the jagged borders
min-height
:
25px
;
}
&
.collapsed
{
background-color
:
$white-light
;
.diff-notes-collapse
,
.note
,
.discussion-reply-holder
,
{
display
:
none
;
}
.notes
>
.badge
{
display
:
block
;
}
}
}
.discussion-body
.image
.frame
{
position
:
relative
;
}
app/assets/stylesheets/pages/note_form.scss
View file @
b54203f0
...
@@ -161,10 +161,13 @@
...
@@ -161,10 +161,13 @@
}
}
.discussion-form
{
.discussion-form
{
padding
:
$gl-padding-top
$gl-padding
$gl-padding
;
background-color
:
$white-light
;
background-color
:
$white-light
;
}
}
.discussion-form-container
{
padding
:
$gl-padding-top
$gl-padding
$gl-padding
;
}
.discussion-notes
.disabled-comment
{
.discussion-notes
.disabled-comment
{
padding
:
6px
0
;
padding
:
6px
0
;
}
}
...
...
app/assets/stylesheets/pages/notes.scss
View file @
b54203f0
...
@@ -650,29 +650,12 @@ ul.notes {
...
@@ -650,29 +650,12 @@ ul.notes {
}
}
.add-diff-note
{
.add-diff-note
{
@include
btn-comment-icon
;
opacity
:
0
;
opacity
:
0
;
margin-top
:
-2px
;
margin-top
:
-2px
;
border-radius
:
50%
;
background
:
$white-light
;
padding
:
1px
5px
;
font-size
:
12px
;
color
:
$blue-500
;
margin-left
:
-55px
;
margin-left
:
-55px
;
position
:
absolute
;
position
:
absolute
;
z-index
:
10
;
z-index
:
10
;
width
:
23px
;
height
:
23px
;
border
:
1px
solid
$blue-500
;
&
:hover
{
background
:
$blue-500
;
border-color
:
$blue-600
;
color
:
$white-light
;
}
&
:active
{
outline
:
0
;
}
}
}
.discussion-body
,
.discussion-body
,
...
...
app/controllers/concerns/notes_actions.rb
View file @
b54203f0
...
@@ -96,7 +96,8 @@ module NotesActions
...
@@ -96,7 +96,8 @@ module NotesActions
id:
note
.
id
,
id:
note
.
id
,
discussion_id:
note
.
discussion_id
(
noteable
),
discussion_id:
note
.
discussion_id
(
noteable
),
html:
note_html
(
note
),
html:
note_html
(
note
),
note:
note
.
note
note:
note
.
note
,
on_image:
note
.
try
(
:on_image?
)
)
)
discussion
=
note
.
to_discussion
(
noteable
)
discussion
=
note
.
to_discussion
(
noteable
)
...
@@ -122,7 +123,9 @@ module NotesActions
...
@@ -122,7 +123,9 @@ module NotesActions
def
diff_discussion_html
(
discussion
)
def
diff_discussion_html
(
discussion
)
return
unless
discussion
.
diff_discussion?
return
unless
discussion
.
diff_discussion?
if
params
[
:view
]
==
'parallel'
on_image
=
discussion
.
on_image?
if
params
[
:view
]
==
'parallel'
&&
!
on_image
template
=
"discussions/_parallel_diff_discussion"
template
=
"discussions/_parallel_diff_discussion"
locals
=
locals
=
if
params
[
:line_type
]
==
'old'
if
params
[
:line_type
]
==
'old'
...
@@ -132,7 +135,9 @@ module NotesActions
...
@@ -132,7 +135,9 @@ module NotesActions
end
end
else
else
template
=
"discussions/_diff_discussion"
template
=
"discussions/_diff_discussion"
locals
=
{
discussions:
[
discussion
]
}
@fresh_discussion
=
true
locals
=
{
discussions:
[
discussion
],
on_image:
on_image
}
end
end
render_to_string
(
render_to_string
(
...
...
app/models/concerns/discussion_on_diff.rb
View file @
b54203f0
...
@@ -28,6 +28,10 @@ module DiscussionOnDiff
...
@@ -28,6 +28,10 @@ module DiscussionOnDiff
true
true
end
end
def
file_new_path
first_note
.
position
.
new_path
end
# Returns an array of at most 16 highlighted lines above a diff note
# Returns an array of at most 16 highlighted lines above a diff note
def
truncated_diff_lines
(
highlight:
true
)
def
truncated_diff_lines
(
highlight:
true
)
lines
=
highlight
?
highlighted_diff_lines
:
diff_lines
lines
=
highlight
?
highlighted_diff_lines
:
diff_lines
...
...
app/models/diff_discussion.rb
View file @
b54203f0
...
@@ -11,6 +11,8 @@ class DiffDiscussion < Discussion
...
@@ -11,6 +11,8 @@ class DiffDiscussion < Discussion
delegate
:position
,
delegate
:position
,
:original_position
,
:original_position
,
:change_position
,
:change_position
,
:on_text?
,
:on_image?
,
to: :first_note
to: :first_note
...
...
app/models/diff_note.rb
View file @
b54203f0
...
@@ -12,8 +12,8 @@ class DiffNote < Note
...
@@ -12,8 +12,8 @@ class DiffNote < Note
validates
:original_position
,
presence:
true
validates
:original_position
,
presence:
true
validates
:position
,
presence:
true
validates
:position
,
presence:
true
validates
:diff_line
,
presence:
true
validates
:diff_line
,
presence:
true
,
if: :on_text?
validates
:line_code
,
presence:
true
,
line_code:
true
validates
:line_code
,
presence:
true
,
line_code:
true
,
if: :on_text?
validates
:noteable_type
,
inclusion:
{
in:
NOTEABLE_TYPES
}
validates
:noteable_type
,
inclusion:
{
in:
NOTEABLE_TYPES
}
validate
:positions_complete
validate
:positions_complete
validate
:verify_supported
validate
:verify_supported
...
@@ -43,6 +43,14 @@ class DiffNote < Note
...
@@ -43,6 +43,14 @@ class DiffNote < Note
end
end
end
end
def
on_text?
position
.
position_type
==
"text"
end
def
on_image?
position
.
position_type
==
"image"
end
def
diff_file
def
diff_file
@diff_file
||=
self
.
original_position
.
diff_file
(
self
.
project
.
repository
)
@diff_file
||=
self
.
original_position
.
diff_file
(
self
.
project
.
repository
)
end
end
...
@@ -56,6 +64,8 @@ class DiffNote < Note
...
@@ -56,6 +64,8 @@ class DiffNote < Note
end
end
def
original_line_code
def
original_line_code
return
unless
on_text?
self
.
diff_file
.
line_code
(
self
.
diff_line
)
self
.
diff_file
.
line_code
(
self
.
diff_line
)
end
end
...
...
app/models/discussion.rb
View file @
b54203f0
...
@@ -66,6 +66,10 @@ class Discussion
...
@@ -66,6 +66,10 @@ class Discussion
@context_noteable
=
context_noteable
@context_noteable
=
context_noteable
end
end
def
on_image?
false
end
def
==
(
other
)
def
==
(
other
)
other
.
class
==
self
.
class
&&
other
.
class
==
self
.
class
&&
other
.
context_noteable
==
self
.
context_noteable
&&
other
.
context_noteable
==
self
.
context_noteable
&&
...
...
app/models/legacy_diff_discussion.rb
View file @
b54203f0
...
@@ -17,6 +17,14 @@ class LegacyDiffDiscussion < Discussion
...
@@ -17,6 +17,14 @@ class LegacyDiffDiscussion < Discussion
true
true
end
end
def
on_image?
false
end
def
on_text?
true
end
def
active?
(
*
args
)
def
active?
(
*
args
)
return
@active
if
@active
.
present?
return
@active
if
@active
.
present?
...
...
app/models/note.rb
View file @
b54203f0
...
@@ -134,14 +134,22 @@ class Note < ActiveRecord::Base
...
@@ -134,14 +134,22 @@ class Note < ActiveRecord::Base
Discussion
.
build
(
notes
)
Discussion
.
build
(
notes
)
end
end
# Group diff discussions by line code or file path.
# It is not needed to group by line code when comment is
# on an image.
def
grouped_diff_discussions
(
diff_refs
=
nil
)
def
grouped_diff_discussions
(
diff_refs
=
nil
)
groups
=
{}
groups
=
{}
diff_notes
.
fresh
.
discussions
.
each
do
|
discussion
|
diff_notes
.
fresh
.
discussions
.
each
do
|
discussion
|
line_code
=
discussion
.
line_code_in_diffs
(
diff_refs
)
group_key
=
if
discussion
.
on_image?
if
line_code
discussion
.
file_new_path
discussions
=
groups
[
line_code
]
||=
[]
else
discussion
.
line_code_in_diffs
(
diff_refs
)
end
if
group_key
discussions
=
groups
[
group_key
]
||=
[]
discussions
<<
discussion
discussions
<<
discussion
end
end
end
end
...
...
app/views/discussions/_diff_discussion.html.haml
View file @
b54203f0
-
expanded
=
local_assigns
.
fetch
(
:expanded
,
true
)
-
if
local_assigns
[
:on_image
]
%tr
.notes_holder
{
class:
(
'hide'
unless
expanded
)
}
=
render
partial:
"discussions/notes"
,
collection:
discussions
,
as: :discussion
%td
.notes_line
{
colspan:
2
}
-
else
%td
.notes_content
-# Text diff discussions
.content
{
class:
(
'hide'
unless
expanded
)
}
-
expanded
=
local_assigns
.
fetch
(
:expanded
,
true
)
=
render
partial:
"discussions/notes"
,
collection:
discussions
,
as: :discussion
%tr
.notes_holder
{
class:
(
'hide'
unless
expanded
)
}
%td
.notes_line
{
colspan:
2
}
%td
.notes_content
.content
{
class:
(
'hide'
unless
expanded
)
}
=
render
partial:
"discussions/notes"
,
collection:
discussions
,
as: :discussion
app/views/discussions/_diff_with_notes.html.haml
View file @
b54203f0
-
diff_file
=
discussion
.
diff_file
-
diff_file
=
discussion
.
diff_file
-
blob
=
discussion
.
blob
-
blob
=
discussion
.
blob
-
discussions
=
{
discussion
.
original_line_code
=>
[
discussion
]
}
-
diff_file_class
=
diff_file
.
text?
?
'text-file'
:
'js-image-file'
.diff-file.file-holder
.diff-file.file-holder
{
class:
diff_file_class
}
.js-file-title.file-title.file-title-flex-parent
.js-file-title.file-title.file-title-flex-parent
.file-header-content
.file-header-content
=
render
"projects/diffs/file_header"
,
diff_file:
diff_file
,
url:
discussion_path
(
discussion
),
show_toggle:
false
=
render
"projects/diffs/file_header"
,
diff_file:
diff_file
,
url:
discussion_path
(
discussion
),
show_toggle:
false
.diff-content.code.js-syntax-highlight
-
if
diff_file
.
text?
%table
.diff-content.code.js-syntax-highlight
-
discussions
=
{
discussion
.
original_line_code
=>
[
discussion
]
}
%table
=
render
partial:
"projects/diffs/line"
,
=
render
partial:
"projects/diffs/line"
,
collection:
discussion
.
truncated_diff_lines
,
collection:
discussion
.
truncated_diff_lines
,
as: :line
,
as: :line
,
locals:
{
diff_file:
diff_file
,
locals:
{
diff_file:
diff_file
,
discussions:
discussions
,
discussions:
discussions
,
discussion_expanded:
true
,
discussion_expanded:
true
,
plain:
true
}
plain:
true
}
-
else
-
partial
=
(
diff_file
.
new_file?
||
diff_file
.
deleted_file?
)
?
'single_image_diff'
:
'replaced_image_diff'
=
render
partial:
"projects/diffs/
#{
partial
}
"
,
locals:
{
diff_file:
diff_file
,
position:
discussion
.
position
.
to_json
,
click_to_comment:
false
}
.note-container
=
render
partial:
"discussions/notes"
,
locals:
{
discussion:
discussion
,
show_toggle:
false
,
show_image_comment_badge:
true
,
disable_collapse:
true
}
app/views/discussions/_notes.html.haml
View file @
b54203f0
.discussion-notes
-
disable_collapse
=
local_assigns
.
fetch
(
:disable_collapse
,
false
)
%ul
.notes
{
data:
{
discussion_id:
discussion
.
id
}
}
-
collapsed_class
=
'collapsed'
if
discussion
.
resolved?
&&
!
disable_collapse
=
render
partial:
"shared/notes/note"
,
collection:
discussion
.
notes
,
as: :note
-
badge_counter
=
discussion_counter
+
1
if
local_assigns
[
:discussion_counter
]
-
show_toggle
=
local_assigns
.
fetch
(
:show_toggle
,
true
)
-
show_image_comment_badge
=
local_assigns
.
fetch
(
:show_image_comment_badge
,
false
)
.discussion-notes
{
class:
collapsed_class
}
-# Save the first note position data so that we have a reference and can go
-# to the first note position when we click on a badge diff discussion
%ul
.notes
{
id:
"discussion_#{discussion.id}"
,
data:
{
discussion_id:
discussion
.
id
,
position:
discussion
.
notes
[
0
].
position
.
to_json
}
}
-
if
discussion
.
try
(
:on_image?
)
&&
show_toggle
%button
.diff-notes-collapse.js-diff-notes-toggle
{
type:
'button'
}
=
sprite_icon
(
'collapse'
,
css_class:
'collapse-icon'
)
%button
.btn-transparent.badge.js-diff-notes-toggle
{
type:
'button'
}
=
badge_counter
=
render
partial:
"shared/notes/note"
,
collection:
discussion
.
notes
,
as: :note
,
locals:
{
badge_counter:
badge_counter
,
show_image_comment_badge:
show_image_comment_badge
}
.flash-container
.flash-container
...
...
app/views/projects/diffs/_image_diff_frame.html.haml
0 → 100644
View file @
b54203f0
-
class_name
=
local_assigns
.
fetch
(
:class_name
,
''
)
-
note_type
=
local_assigns
.
fetch
(
:note_type
,
''
)
.frame
{
class:
class_name
,
data:
{
position:
position
,
note_type:
note_type
}
}
=
image_tag
(
image_path
,
alt:
alt
,
draggable:
false
,
lazy:
false
)
app/views/projects/diffs/_replaced_image_diff.html.haml
0 → 100644
View file @
b54203f0
-
blob
=
diff_file
.
blob
-
old_blob
=
diff_file
.
old_blob
-
blob_raw_path
=
diff_file_blob_raw_path
(
diff_file
)
-
old_blob_raw_path
=
diff_file_old_blob_raw_path
(
diff_file
)
-
click_to_comment
=
local_assigns
.
fetch
(
:click_to_comment
,
true
)
-
diff_view_data
=
local_assigns
.
fetch
(
:diff_view_data
,
''
)
-
class_name
=
''
-
if
click_to_comment
-
class_name
=
'js-add-image-diff-note-button click-to-comment'
.image.js-replaced-image
{
data:
diff_view_data
}
.two-up.view
.wrap
.frame.deleted
=
image_tag
(
old_blob_raw_path
,
alt:
diff_file
.
old_path
,
lazy:
false
)
%p
.image-info.hide
%span
.meta-filesize
=
number_to_human_size
(
old_blob
.
size
)
|
%strong
W:
%span
.meta-width
|
%strong
H:
%span
.meta-height
.wrap
=
render
partial:
"projects/diffs/image_diff_frame"
,
locals:
{
class_name:
"added js-image-frame
#{
class_name
}
"
,
position:
position
,
note_type:
DiffNote
.
name
,
image_path:
blob_raw_path
,
alt:
diff_file
.
new_path
}
%p
.image-info.hide
%span
.meta-filesize
=
number_to_human_size
(
blob
.
size
)
|
%strong
W:
%span
.meta-width
|
%strong
H:
%span
.meta-height
.swipe.view.hide
.swipe-frame
.frame.deleted
=
image_tag
(
old_blob_raw_path
,
alt:
diff_file
.
old_path
,
lazy:
false
)
.swipe-wrap
=
render
partial:
"projects/diffs/image_diff_frame"
,
locals:
{
class_name:
"added js-image-frame
#{
class_name
}
"
,
position:
position
,
note_type:
DiffNote
.
name
,
image_path:
blob_raw_path
,
alt:
diff_file
.
new_path
}
%span
.swipe-bar
%span
.top-handle
%span
.bottom-handle
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
=
image_tag
(
old_blob_raw_path
,
alt:
diff_file
.
old_path
,
lazy:
false
)
=
render
partial:
"projects/diffs/image_diff_frame"
,
locals:
{
class_name:
"added js-image-frame
#{
class_name
}
"
,
position:
position
,
note_type:
DiffNote
.
name
,
image_path:
blob_raw_path
,
alt:
diff_file
.
new_path
}
.controls
.transparent
.drag-track
.dragger
{
:style
=>
"left: 0px;"
}
.opaque
.view-modes.hide
%ul
.view-modes-menu
%li
.two-up
{
data:
{
mode:
'two-up'
}
}
2-up
%li
.swipe
{
data:
{
mode:
'swipe'
}
}
Swipe
%li
.onion-skin
{
data:
{
mode:
'onion-skin'
}
}
Onion skin
app/views/projects/diffs/_single_image_diff.html.haml
0 → 100644
View file @
b54203f0
-
blob
=
diff_file
.
blob
-
old_blob
=
diff_file
.
old_blob
-
blob_raw_path
=
diff_file_blob_raw_path
(
diff_file
)
-
old_blob_raw_path
=
diff_file_old_blob_raw_path
(
diff_file
)
-
click_to_comment
=
local_assigns
.
fetch
(
:click_to_comment
,
true
)
-
diff_view_data
=
local_assigns
.
fetch
(
:diff_view_data
,
''
)
-
class_name
=
''
-
if
click_to_comment
-
class_name
=
'js-add-image-diff-note-button click-to-comment'
.image.js-single-image
{
data:
diff_view_data
}
.wrap
-
single_class_name
=
diff_file
.
deleted_file?
?
'deleted'
:
'added'
=
render
partial:
"projects/diffs/image_diff_frame"
,
locals:
{
class_name:
"
#{
single_class_name
}
#{
class_name
}
js-image-frame"
,
position:
position
,
note_type:
DiffNote
.
name
,
image_path:
blob_raw_path
,
alt:
diff_file
.
file_path
}
%p
.image-info
=
number_to_human_size
(
blob
.
size
)
app/views/projects/diffs/viewers/_image.html.haml
View file @
b54203f0
-
diff_file
=
viewer
.
diff_file
-
diff_file
=
viewer
.
diff_file
-
blob
=
diff_file
.
blob
-
image_point
=
Gitlab
::
Diff
::
ImagePoint
.
new
(
nil
,
nil
,
nil
,
nil
)
-
old_blob
=
diff_file
.
old_blob
-
discussions
=
@grouped_diff_discussions
[
diff_file
.
new_path
]
if
@grouped_diff_discussions
-
blob_raw_path
=
diff_file_blob_raw_path
(
diff_file
)
-
old_blob_raw_path
=
diff_file_old_blob_raw_path
(
diff_file
)
-
locals
=
{
diff_file:
diff_file
,
position:
diff_file
.
position
(
image_point
,
position_type: :image
).
to_json
,
click_to_comment:
true
,
diff_view_data:
diff_view_data
}
-
if
diff_file
.
new_file?
||
diff_file
.
deleted_file?
-
if
diff_file
.
new_file?
||
diff_file
.
deleted_file?
.image
=
render
partial:
"projects/diffs/single_image_diff"
,
locals:
locals
%span
.wrap
.frame
{
class:
(
diff_file
.
deleted_file?
?
'deleted'
:
'added'
)
}
=
image_tag
(
blob_raw_path
,
alt:
diff_file
.
file_path
)
%p
.image-info
=
number_to_human_size
(
blob
.
size
)
-
else
-
else
.image
=
render
partial:
"projects/diffs/replaced_image_diff"
,
locals:
locals
.two-up.view
%span
.wrap
.frame.deleted
=
image_tag
(
old_blob_raw_path
,
alt:
diff_file
.
old_path
)
%p
.image-info.hide
%span
.meta-filesize
=
number_to_human_size
(
old_blob
.
size
)
|
%b
W:
%span
.meta-width
|
%b
H:
%span
.meta-height
%span
.wrap
.frame.added
=
image_tag
(
blob_raw_path
,
alt:
diff_file
.
new_path
)
%p
.image-info.hide
%span
.meta-filesize
=
number_to_human_size
(
blob
.
size
)
|
%b
W:
%span
.meta-width
|
%b
H:
%span
.meta-height
.swipe.view.hide
.swipe-frame
.frame.deleted
=
image_tag
(
old_blob_raw_path
,
alt:
diff_file
.
old_path
,
lazy:
false
)
.swipe-wrap
.frame.added
=
image_tag
(
blob_raw_path
,
alt:
diff_file
.
new_path
,
lazy:
false
)
%span
.swipe-bar
%span
.top-handle
%span
.bottom-handle
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
=
image_tag
(
old_blob_raw_path
,
alt:
diff_file
.
old_path
,
lazy:
false
)
.frame.added
=
image_tag
(
blob_raw_path
,
alt:
diff_file
.
new_path
,
lazy:
false
)
.controls
.transparent
.drag-track
.dragger
{
:style
=>
"left: 0px;"
}
.opaque
.view-modes.hide
.note-container
%ul
.view-modes-menu
=
render
partial:
"discussions/notes"
,
collection:
discussions
,
as: :discussion
%li
.two-up
{
data:
{
mode:
'two-up'
}
}
2-up
%li
.swipe
{
data:
{
mode:
'swipe'
}
}
Swipe
%li
.onion-skin
{
data:
{
mode:
'onion-skin'
}
}
Onion skin
app/views/shared/notes/_form.html.haml
View file @
b54203f0
...
@@ -24,20 +24,21 @@
...
@@ -24,20 +24,21 @@
-# DiffNote
-# DiffNote
=
f
.
hidden_field
:position
=
f
.
hidden_field
:position
=
render
layout:
'projects/md_preview'
,
locals:
{
url:
preview_url
,
referenced_users:
true
}
do
.discussion-form-container
=
render
'projects/zen'
,
f:
f
,
=
render
layout:
'projects/md_preview'
,
locals:
{
url:
preview_url
,
referenced_users:
true
}
do
attr: :note
,
=
render
'projects/zen'
,
f:
f
,
classes:
'note-textarea js-note-text'
,
attr: :note
,
placeholder:
"Write a comment or drag your files here..."
,
classes:
'note-textarea js-note-text'
,
supports_quick_actions:
supports_quick_actions
,
placeholder:
"Write a comment or drag your files here..."
,
supports_autocomplete:
supports_autocomplete
supports_quick_actions:
supports_quick_actions
,
=
render
'shared/notes/hints'
,
supports_quick_actions:
supports_quick_actions
supports_autocomplete:
supports_autocomplete
.error-alert
=
render
'shared/notes/hints'
,
supports_quick_actions:
supports_quick_actions
.error-alert
.note-form-actions.clearfix
=
render
partial:
'shared/notes/comment_button'
.note-form-actions.clearfix
=
render
partial:
'shared/notes/comment_button'
=
yield
(
:note_actions
)
=
yield
(
:note_actions
)
%a
.btn.btn-cancel.js-note-discard
{
role:
"button"
,
data:
{
cancel_text:
"Cancel"
}
}
Discard draft
%a
.btn.btn-cancel.js-note-discard
{
role:
"button"
,
data:
{
cancel_text:
"Cancel"
}
}
Discard draft
app/views/shared/notes/_note.html.haml
View file @
b54203f0
-
return
unless
note
.
author
-
return
unless
note
.
author
-
return
if
note
.
cross_reference_not_visible_for?
(
current_user
)
-
return
if
note
.
cross_reference_not_visible_for?
(
current_user
)
-
show_image_comment_badge
=
local_assigns
.
fetch
(
:show_image_comment_badge
,
false
)
-
note_editable
=
note_editable?
(
note
)
-
note_editable
=
note_editable?
(
note
)
-
note_counter
=
local_assigns
.
fetch
(
:note_counter
,
0
)
%li
.timeline-entry
{
id:
dom_id
(
note
),
%li
.timeline-entry
{
id:
dom_id
(
note
),
class:
[
"note"
,
"note-row-#{note.id}"
,
(
'system-note'
if
note
.
system
)],
class:
[
"note"
,
"note-row-#{note.id}"
,
(
'system-note'
if
note
.
system
)],
data:
{
author_id:
note
.
author
.
id
,
data:
{
author_id:
note
.
author
.
id
,
...
@@ -12,8 +15,18 @@
...
@@ -12,8 +15,18 @@
-
if
note
.
system
-
if
note
.
system
=
icon_for_system_note
(
note
)
=
icon_for_system_note
(
note
)
-
else
-
else
%a
{
href:
user_path
(
note
.
author
)
}
%a
.image-diff-avatar-link
{
href:
user_path
(
note
.
author
)
}
=
image_tag
avatar_icon
(
note
.
author
),
alt:
''
,
class:
'avatar s40'
=
image_tag
avatar_icon
(
note
.
author
),
alt:
''
,
class:
'avatar s40'
-
if
note
.
is_a?
(
DiffNote
)
&&
note
.
on_image?
-
if
show_image_comment_badge
&&
note_counter
==
0
-# Only show this for the first comment in the discussion
%span
.image-comment-badge.inverted
=
icon
(
'comment-o'
)
-
elsif
note_counter
==
0
-
counter
=
badge_counter
if
local_assigns
[
:badge_counter
]
-
badge_class
=
"hidden"
if
@fresh_discussion
||
counter
.
nil?
%span
.badge
{
class:
badge_class
}
=
counter
.timeline-content
.timeline-content
.note-header
.note-header
.note-header-info
.note-header-info
...
...
changelogs/unreleased/issue_35873.yml
0 → 100644
View file @
b54203f0
---
title
:
Commenting on image diffs
merge_request
:
14061
author
:
type
:
added
lib/gitlab/diff/file.rb
View file @
b54203f0
...
@@ -27,16 +27,23 @@ module Gitlab
...
@@ -27,16 +27,23 @@ module Gitlab
@fallback_diff_refs
=
fallback_diff_refs
@fallback_diff_refs
=
fallback_diff_refs
end
end
def
position
(
line
)
def
position
(
position_marker
,
position_type: :text
)
return
unless
diff_refs
return
unless
diff_refs
Position
.
new
(
data
=
{
diff_refs:
diff_refs
,
position_type:
position_type
.
to_s
,
old_path:
old_path
,
old_path:
old_path
,
new_path:
new_path
,
new_path:
new_path
old_line:
line
.
old_line
,
}
new_line:
line
.
new_line
,
diff_refs:
diff_refs
if
position_type
==
:text
)
data
.
merge!
(
text_position_properties
(
position_marker
))
else
data
.
merge!
(
image_position_properties
(
position_marker
))
end
Position
.
new
(
data
)
end
end
def
line_code
(
line
)
def
line_code
(
line
)
...
@@ -228,6 +235,14 @@ module Gitlab
...
@@ -228,6 +235,14 @@ module Gitlab
private
private
def
text_position_properties
(
line
)
{
old_line:
line
.
old_line
,
new_line:
line
.
new_line
}
end
def
image_position_properties
(
image_point
)
image_point
.
to_h
end
def
blobs_changed?
def
blobs_changed?
old_blob
&&
new_blob
&&
old_blob
.
id
!=
new_blob
.
id
old_blob
&&
new_blob
&&
old_blob
.
id
!=
new_blob
.
id
end
end
...
...
lib/gitlab/diff/formatters/base_formatter.rb
0 → 100644
View file @
b54203f0
module
Gitlab
module
Diff
module
Formatters
class
BaseFormatter
attr_reader
:old_path
attr_reader
:new_path
attr_reader
:base_sha
attr_reader
:start_sha
attr_reader
:head_sha
attr_reader
:position_type
def
initialize
(
attrs
)
if
diff_file
=
attrs
[
:diff_file
]
attrs
[
:diff_refs
]
=
diff_file
.
diff_refs
attrs
[
:old_path
]
=
diff_file
.
old_path
attrs
[
:new_path
]
=
diff_file
.
new_path
end
if
diff_refs
=
attrs
[
:diff_refs
]
attrs
[
:base_sha
]
=
diff_refs
.
base_sha
attrs
[
:start_sha
]
=
diff_refs
.
start_sha
attrs
[
:head_sha
]
=
diff_refs
.
head_sha
end
@old_path
=
attrs
[
:old_path
]
@new_path
=
attrs
[
:new_path
]
@base_sha
=
attrs
[
:base_sha
]
@start_sha
=
attrs
[
:start_sha
]
@head_sha
=
attrs
[
:head_sha
]
end
def
key
[
base_sha
,
start_sha
,
head_sha
,
Digest
::
SHA1
.
hexdigest
(
old_path
||
""
),
Digest
::
SHA1
.
hexdigest
(
new_path
||
""
)]
end
def
to_h
{
base_sha:
base_sha
,
start_sha:
start_sha
,
head_sha:
head_sha
,
old_path:
old_path
,
new_path:
new_path
,
position_type:
position_type
}
end
def
position_type
raise
NotImplementedError
end
def
==
(
other
)
raise
NotImplementedError
end
def
complete?
raise
NotImplementedError
end
end
end
end
end
lib/gitlab/diff/formatters/image_formatter.rb
0 → 100644
View file @
b54203f0
module
Gitlab
module
Diff
module
Formatters
class
ImageFormatter
<
BaseFormatter
attr_reader
:width
attr_reader
:height
attr_reader
:x
attr_reader
:y
def
initialize
(
attrs
)
@x
=
attrs
[
:x
]
@y
=
attrs
[
:y
]
@width
=
attrs
[
:width
]
@height
=
attrs
[
:height
]
super
(
attrs
)
end
def
key
@key
||=
super
.
push
(
x
,
y
)
end
def
complete?
x
&&
y
&&
width
&&
height
end
def
to_h
super
.
merge
(
width:
width
,
height:
height
,
x:
x
,
y:
y
)
end
def
position_type
"image"
end
def
==
(
other
)
other
.
is_a?
(
self
.
class
)
&&
x
==
other
.
x
&&
y
==
other
.
y
end
end
end
end
end
lib/gitlab/diff/formatters/text_formatter.rb
0 → 100644
View file @
b54203f0
module
Gitlab
module
Diff
module
Formatters
class
TextFormatter
<
BaseFormatter
attr_reader
:old_line
attr_reader
:new_line
def
initialize
(
attrs
)
@old_line
=
attrs
[
:old_line
]
@new_line
=
attrs
[
:new_line
]
super
(
attrs
)
end
def
key
@key
||=
super
.
push
(
old_line
,
new_line
)
end
def
complete?
old_line
||
new_line
end
def
to_h
super
.
merge
(
old_line:
old_line
,
new_line:
new_line
)
end
def
line_age
if
old_line
&&
new_line
nil
elsif
new_line
'new'
else
'old'
end
end
def
position_type
"text"
end
def
==
(
other
)
other
.
is_a?
(
self
.
class
)
&&
new_line
==
other
.
new_line
&&
old_line
==
other
.
old_line
end
end
end
end
end
lib/gitlab/diff/image_point.rb
0 → 100644
View file @
b54203f0
module
Gitlab
module
Diff
class
ImagePoint
attr_reader
:width
,
:height
,
:x
,
:y
def
initialize
(
width
,
height
,
x
,
y
)
@width
=
width
@height
=
height
@x
=
x
@y
=
y
end
def
to_h
{
width:
width
,
height:
height
,
x:
x
,
y:
y
}
end
end
end
end
lib/gitlab/diff/position.rb
View file @
b54203f0
# Defines a specific location, identified by paths
and line number
s,
# Defines a specific location, identified by paths
line numbers and image coordinate
s,
# within a specific diff, identified by start, head and base commit ids.
# within a specific diff, identified by start, head and base commit ids.
module
Gitlab
module
Gitlab
module
Diff
module
Diff
class
Position
class
Position
attr_reader
:old_path
attr_accessor
:formatter
attr_reader
:new_path
attr_reader
:old_line
delegate
:old_path
,
attr_reader
:new_line
:new_path
,
attr_reader
:base_sha
:base_sha
,
attr_reader
:start_sha
:start_sha
,
attr_reader
:head_sha
:head_sha
,
:old_line
,
:new_line
,
:position_type
,
to: :formatter
# A position can belong to a text line or to an image coordinate
# it depends of the position_type argument.
# Text position will have: new_line and old_line
# Image position will have: width, height, x, y
def
initialize
(
attrs
=
{})
def
initialize
(
attrs
=
{})
if
diff_file
=
attrs
[
:diff_file
]
@formatter
=
get_formatter_class
(
attrs
[
:position_type
]).
new
(
attrs
)
attrs
[
:diff_refs
]
=
diff_file
.
diff_refs
attrs
[
:old_path
]
=
diff_file
.
old_path
attrs
[
:new_path
]
=
diff_file
.
new_path
end
if
diff_refs
=
attrs
[
:diff_refs
]
attrs
[
:base_sha
]
=
diff_refs
.
base_sha
attrs
[
:start_sha
]
=
diff_refs
.
start_sha
attrs
[
:head_sha
]
=
diff_refs
.
head_sha
end
@old_path
=
attrs
[
:old_path
]
@new_path
=
attrs
[
:new_path
]
@base_sha
=
attrs
[
:base_sha
]
@start_sha
=
attrs
[
:start_sha
]
@head_sha
=
attrs
[
:head_sha
]
@old_line
=
attrs
[
:old_line
]
@new_line
=
attrs
[
:new_line
]
end
end
# `Gitlab::Diff::Position` objects are stored as serialized attributes in
# `Gitlab::Diff::Position` objects are stored as serialized attributes in
...
@@ -46,7 +34,11 @@ module Gitlab
...
@@ -46,7 +34,11 @@ module Gitlab
end
end
def
encode_with
(
coder
)
def
encode_with
(
coder
)
coder
[
'attributes'
]
=
self
.
to_h
coder
[
'attributes'
]
=
formatter
.
to_h
end
def
key
formatter
.
key
end
end
def
==
(
other
)
def
==
(
other
)
...
@@ -54,20 +46,11 @@ module Gitlab
...
@@ -54,20 +46,11 @@ module Gitlab
other
.
diff_refs
==
diff_refs
&&
other
.
diff_refs
==
diff_refs
&&
other
.
old_path
==
old_path
&&
other
.
old_path
==
old_path
&&
other
.
new_path
==
new_path
&&
other
.
new_path
==
new_path
&&
other
.
old_line
==
old_line
&&
other
.
formatter
==
formatter
other
.
new_line
==
new_line
end
end
def
to_h
def
to_h
{
formatter
.
to_h
old_path:
old_path
,
new_path:
new_path
,
old_line:
old_line
,
new_line:
new_line
,
base_sha:
base_sha
,
start_sha:
start_sha
,
head_sha:
head_sha
}
end
end
def
inspect
def
inspect
...
@@ -75,23 +58,15 @@ module Gitlab
...
@@ -75,23 +58,15 @@ module Gitlab
end
end
def
complete?
def
complete?
file_path
.
present?
&&
file_path
.
present?
&&
formatter
.
complete?
&&
diff_refs
.
complete?
(
old_line
||
new_line
)
&&
diff_refs
.
complete?
end
end
def
to_json
(
opts
=
nil
)
def
to_json
(
opts
=
nil
)
JSON
.
generate
(
self
.
to_h
,
opts
)
JSON
.
generate
(
formatter
.
to_h
,
opts
)
end
end
def
type
def
type
if
old_line
&&
new_line
formatter
.
line_age
nil
elsif
new_line
'new'
else
'old'
end
end
end
def
unchanged?
def
unchanged?
...
@@ -150,6 +125,17 @@ module Gitlab
...
@@ -150,6 +125,17 @@ module Gitlab
diff_refs
.
compare_in
(
repository
.
project
).
diffs
(
paths:
paths
,
expanded:
true
).
diff_files
.
first
diff_refs
.
compare_in
(
repository
.
project
).
diffs
(
paths:
paths
,
expanded:
true
).
diff_files
.
first
end
end
def
get_formatter_class
(
type
)
type
||=
"text"
case
type
when
'image'
Gitlab
::
Diff
::
Formatters
::
ImageFormatter
else
Gitlab
::
Diff
::
Formatters
::
TextFormatter
end
end
end
end
end
end
end
end
spec/factories/merge_requests.rb
View file @
b54203f0
...
@@ -22,6 +22,11 @@ FactoryGirl.define do
...
@@ -22,6 +22,11 @@ FactoryGirl.define do
trait
:with_diffs
do
trait
:with_diffs
do
end
end
trait
:with_image_diffs
do
source_branch
"add_images_and_changes"
target_branch
"master"
end
trait
:without_diffs
do
trait
:without_diffs
do
source_branch
"improve/awesome"
source_branch
"improve/awesome"
target_branch
"master"
target_branch
"master"
...
...
spec/features/merge_requests/diffs_spec.rb
View file @
b54203f0
...
@@ -42,8 +42,12 @@ feature 'Diffs URL', js: true do
...
@@ -42,8 +42,12 @@ feature 'Diffs URL', js: true do
visit
"
#{
diffs_project_merge_request_path
(
project
,
merge_request
)
}#{
fragment
}
"
visit
"
#{
diffs_project_merge_request_path
(
project
,
merge_request
)
}#{
fragment
}
"
end
end
it
'shows expanded note'
do
it
'shows collapsed note'
do
expect
(
page
).
to
have_selector
(
fragment
,
visible:
true
)
wait_for_requests
expect
(
page
).
to
have_selector
(
'.discussion-notes.collapsed'
)
do
|
note_container
|
expect
(
note_container
).
to
have_selector
(
fragment
,
visible:
false
)
end
end
end
end
end
end
end
...
...
spec/features/merge_requests/image_diff_notes.rb
0 → 100644
View file @
b54203f0
require
'spec_helper'
feature
'image diff notes'
,
js:
true
do
include
NoteInteractionHelpers
let
(
:user
)
{
create
(
:user
)
}
let
(
:project
)
{
create
(
:project
,
:public
,
:repository
)
}
before
do
project
.
team
<<
[
user
,
:master
]
sign_in
user
page
.
driver
.
set_cookie
(
'sidebar_collapsed'
,
'true'
)
# Stub helper to return any blob file as image from public app folder.
# This is necessary to run this specs since we don't display repo images in capybara.
allow_any_instance_of
(
DiffHelper
).
to
receive
(
:diff_file_blob_raw_path
).
and_return
(
'/apple-touch-icon.png'
)
end
context
'create commit diff notes'
do
commit_id
=
'2f63565e7aac07bcdadb654e253078b727143ec4'
describe
'create a new diff note'
do
before
do
visit
project_commit_path
(
project
,
commit_id
)
create_image_diff_note
end
it
'shows indicator badge on image diff'
do
indicator
=
find
(
'.js-image-badge'
)
expect
(
indicator
).
to
have_content
(
'1'
)
end
it
'shows the avatar badge on the new note'
do
badge
=
find
(
'.image-diff-avatar-link .badge'
)
expect
(
badge
).
to
have_content
(
'1'
)
end
it
'allows collapsing/expanding the discussion notes'
do
find
(
'.js-diff-notes-toggle'
,
:first
).
click
expect
(
page
).
not_to
have_content
(
'image diff test comment'
)
find
(
'.js-diff-notes-toggle'
).
click
expect
(
page
).
to
have_content
(
'image diff test comment'
)
end
end
describe
'render commit diff notes'
do
let
(
:path
)
{
"files/images/6049019_460s.jpg"
}
let
(
:commit
)
{
project
.
commit
(
'2f63565e7aac07bcdadb654e253078b727143ec4'
)
}
let
(
:note1_position
)
do
Gitlab
::
Diff
::
Position
.
new
(
old_path:
path
,
new_path:
path
,
width:
100
,
height:
100
,
x:
10
,
y:
10
,
position_type:
"image"
,
diff_refs:
commit
.
diff_refs
)
end
let
(
:note2_position
)
do
Gitlab
::
Diff
::
Position
.
new
(
old_path:
path
,
new_path:
path
,
width:
100
,
height:
100
,
x:
20
,
y:
20
,
position_type:
"image"
,
diff_refs:
commit
.
diff_refs
)
end
let!
(
:note1
)
{
create
(
:diff_note_on_commit
,
commit_id:
commit
.
id
,
project:
project
,
position:
note1_position
,
note:
'my note 1'
)
}
let!
(
:note2
)
{
create
(
:diff_note_on_commit
,
commit_id:
commit
.
id
,
project:
project
,
position:
note2_position
,
note:
'my note 2'
)
}
before
do
visit
project_commit_path
(
project
,
commit
.
id
)
wait_for_requests
end
it
'render diff indicators within the image diff frame'
do
expect
(
page
).
to
have_css
(
'.js-image-badge'
,
count:
2
)
end
it
'shows the diff notes'
do
expect
(
page
).
to
have_css
(
'.diff-content .note'
,
count:
2
)
end
it
'shows the diff notes with correct avatar badge numbers'
do
expect
(
page
).
to
have_css
(
'.image-diff-avatar-link'
,
text:
1
)
expect
(
page
).
to
have_css
(
'.image-diff-avatar-link'
,
text:
2
)
end
end
end
%w(inline parallel)
.
each
do
|
view
|
context
"
#{
view
}
view"
do
let
(
:merge_request
)
{
create
(
:merge_request_with_diffs
,
:with_image_diffs
,
source_project:
project
,
author:
user
)
}
let
(
:path
)
{
"files/images/ee_repo_logo.png"
}
let
(
:position
)
do
Gitlab
::
Diff
::
Position
.
new
(
old_path:
path
,
new_path:
path
,
width:
100
,
height:
100
,
x:
1
,
y:
1
,
position_type:
"image"
,
diff_refs:
merge_request
.
diff_refs
)
end
let!
(
:note
)
{
create
(
:diff_note_on_merge_request
,
project:
project
,
noteable:
merge_request
,
position:
position
)
}
describe
'creating a new diff note'
do
before
do
visit
diffs_project_merge_request_path
(
project
,
merge_request
,
view:
view
)
create_image_diff_note
end
it
'shows indicator badge on image diff'
do
indicator
=
find
(
'.js-image-badge'
,
match: :first
)
expect
(
indicator
).
to
have_content
(
'1'
)
end
it
'shows the avatar badge on the new note'
do
badge
=
find
(
'.image-diff-avatar-link .badge'
,
match: :first
)
expect
(
badge
).
to
have_content
(
'1'
)
end
it
'allows expanding/collapsing the discussion notes'
do
page
.
all
(
'.js-diff-notes-toggle'
)[
0
].
trigger
(
'click'
)
page
.
all
(
'.js-diff-notes-toggle'
)[
1
].
trigger
(
'click'
)
expect
(
page
).
not_to
have_content
(
'image diff test comment'
)
page
.
all
(
'.js-diff-notes-toggle'
)[
0
].
trigger
(
'click'
)
page
.
all
(
'.js-diff-notes-toggle'
)[
1
].
trigger
(
'click'
)
expect
(
page
).
to
have_content
(
'image diff test comment'
)
end
end
end
end
describe
'discussion tab polling'
,
:js
do
let
(
:merge_request
)
{
create
(
:merge_request_with_diffs
,
:with_image_diffs
,
source_project:
project
,
author:
user
)
}
let
(
:path
)
{
"files/images/ee_repo_logo.png"
}
let
(
:position
)
do
Gitlab
::
Diff
::
Position
.
new
(
old_path:
path
,
new_path:
path
,
width:
100
,
height:
100
,
x:
50
,
y:
50
,
position_type:
"image"
,
diff_refs:
merge_request
.
diff_refs
)
end
before
do
visit
project_merge_request_path
(
project
,
merge_request
)
end
it
'render diff indicators within the image frame'
do
diff_note
=
create
(
:diff_note_on_merge_request
,
project:
project
,
noteable:
merge_request
,
position:
position
)
wait_for_requests
expect
(
page
).
to
have_selector
(
'.image-comment-badge'
)
expect
(
page
).
to
have_content
(
diff_note
.
note
)
end
end
end
def
create_image_diff_note
find
(
'.js-add-image-diff-note-button'
,
match: :first
).
click
page
.
all
(
'.js-add-image-diff-note-button'
)[
0
].
trigger
(
'click'
)
find
(
'.diff-content .note-textarea'
).
native
.
send_keys
(
'image diff test comment'
)
click_button
'Comment'
wait_for_requests
end
spec/features/merge_requests/user_posts_diff_notes_spec.rb
View file @
b54203f0
...
@@ -227,6 +227,7 @@ feature 'Merge requests > User posts diff notes', :js do
...
@@ -227,6 +227,7 @@ feature 'Merge requests > User posts diff notes', :js do
write_comment_on_line
(
line_holder
,
diff_side
)
write_comment_on_line
(
line_holder
,
diff_side
)
click_button
'Comment'
click_button
'Comment'
wait_for_requests
wait_for_requests
assert_comment_persistence
(
line_holder
,
asset_form_reset:
asset_form_reset
)
assert_comment_persistence
(
line_holder
,
asset_form_reset:
asset_form_reset
)
...
...
spec/javascripts/image_diff/helpers/badge_helper_spec.js
0 → 100644
View file @
b54203f0
import
*
as
badgeHelper
from
'
~/image_diff/helpers/badge_helper
'
;
import
*
as
mockData
from
'
../mock_data
'
;
describe
(
'
badge helper
'
,
()
=>
{
const
{
coordinate
,
noteId
,
badgeText
,
badgeNumber
}
=
mockData
;
let
containerEl
;
let
buttonEl
;
beforeEach
(()
=>
{
containerEl
=
document
.
createElement
(
'
div
'
);
});
describe
(
'
createImageBadge
'
,
()
=>
{
beforeEach
(()
=>
{
buttonEl
=
badgeHelper
.
createImageBadge
(
noteId
,
coordinate
);
});
it
(
'
should create button
'
,
()
=>
{
expect
(
buttonEl
.
tagName
).
toEqual
(
'
BUTTON
'
);
expect
(
buttonEl
.
getAttribute
(
'
type
'
)).
toEqual
(
'
button
'
);
});
it
(
'
should set disabled attribute
'
,
()
=>
{
expect
(
buttonEl
.
hasAttribute
(
'
disabled
'
)).
toEqual
(
true
);
});
it
(
'
should set noteId
'
,
()
=>
{
expect
(
buttonEl
.
dataset
.
noteId
).
toEqual
(
noteId
);
});
it
(
'
should set coordinate
'
,
()
=>
{
expect
(
buttonEl
.
style
.
left
).
toEqual
(
`
${
coordinate
.
x
}
px`
);
expect
(
buttonEl
.
style
.
top
).
toEqual
(
`
${
coordinate
.
y
}
px`
);
});
describe
(
'
classNames
'
,
()
=>
{
it
(
'
should set .js-image-badge by default
'
,
()
=>
{
expect
(
buttonEl
.
className
).
toEqual
(
'
js-image-badge
'
);
});
it
(
'
should add additional class names if parameter is passed
'
,
()
=>
{
const
classNames
=
[
'
first-class
'
,
'
second-class
'
];
buttonEl
=
badgeHelper
.
createImageBadge
(
noteId
,
coordinate
,
classNames
);
expect
(
buttonEl
.
className
).
toEqual
(
classNames
.
concat
(
'
js-image-badge
'
).
join
(
'
'
));
});
});
});
describe
(
'
addImageBadge
'
,
()
=>
{
beforeEach
(()
=>
{
badgeHelper
.
addImageBadge
(
containerEl
,
{
coordinate
,
badgeText
,
noteId
,
});
buttonEl
=
containerEl
.
querySelector
(
'
button
'
);
});
it
(
'
should appends button to container
'
,
()
=>
{
expect
(
buttonEl
).
toBeDefined
();
});
it
(
'
should set the badge text
'
,
()
=>
{
expect
(
buttonEl
.
innerText
).
toEqual
(
badgeText
);
});
it
(
'
should set the button coordinates
'
,
()
=>
{
expect
(
buttonEl
.
style
.
left
).
toEqual
(
`
${
coordinate
.
x
}
px`
);
expect
(
buttonEl
.
style
.
top
).
toEqual
(
`
${
coordinate
.
y
}
px`
);
});
it
(
'
should set the button noteId
'
,
()
=>
{
expect
(
buttonEl
.
dataset
.
noteId
).
toEqual
(
noteId
);
});
});
describe
(
'
addImageCommentBadge
'
,
()
=>
{
beforeEach
(()
=>
{
badgeHelper
.
addImageCommentBadge
(
containerEl
,
{
coordinate
,
noteId
,
});
buttonEl
=
containerEl
.
querySelector
(
'
button
'
);
});
it
(
'
should append icon button to container
'
,
()
=>
{
expect
(
buttonEl
).
toBeDefined
();
});
it
(
'
should create icon comment button
'
,
()
=>
{
const
iconEl
=
buttonEl
.
querySelector
(
'
i
'
);
expect
(
iconEl
).
toBeDefined
();
expect
(
iconEl
.
classList
.
contains
(
'
fa
'
)).
toEqual
(
true
);
expect
(
iconEl
.
classList
.
contains
(
'
fa-comment-o
'
)).
toEqual
(
true
);
});
it
(
'
should have .image-comment-badge.inverted in button class
'
,
()
=>
{
expect
(
buttonEl
.
classList
.
contains
(
'
image-comment-badge
'
)).
toEqual
(
true
);
expect
(
buttonEl
.
classList
.
contains
(
'
inverted
'
)).
toEqual
(
true
);
});
});
describe
(
'
addAvatarBadge
'
,
()
=>
{
let
avatarBadgeEl
;
beforeEach
(()
=>
{
containerEl
.
innerHTML
=
`
<div id="
${
noteId
}
">
<div class="badge hidden">
</div>
</div>
`
;
badgeHelper
.
addAvatarBadge
(
containerEl
,
{
detail
:
{
noteId
,
badgeNumber
,
},
});
avatarBadgeEl
=
containerEl
.
querySelector
(
`#
${
noteId
}
.badge`
);
});
it
(
'
should update badge number
'
,
()
=>
{
expect
(
avatarBadgeEl
.
innerText
).
toEqual
(
badgeNumber
.
toString
());
});
it
(
'
should remove hidden class
'
,
()
=>
{
expect
(
avatarBadgeEl
.
classList
.
contains
(
'
hidden
'
)).
toEqual
(
false
);
});
});
});
spec/javascripts/image_diff/helpers/comment_indicator_helper_spec.js
0 → 100644
View file @
b54203f0
import
*
as
commentIndicatorHelper
from
'
~/image_diff/helpers/comment_indicator_helper
'
;
import
*
as
mockData
from
'
../mock_data
'
;
describe
(
'
commentIndicatorHelper
'
,
()
=>
{
const
{
coordinate
}
=
mockData
;
let
containerEl
;
beforeEach
(()
=>
{
containerEl
=
document
.
createElement
(
'
div
'
);
});
describe
(
'
addCommentIndicator
'
,
()
=>
{
let
buttonEl
;
beforeEach
(()
=>
{
commentIndicatorHelper
.
addCommentIndicator
(
containerEl
,
coordinate
);
buttonEl
=
containerEl
.
querySelector
(
'
button
'
);
});
it
(
'
should append button to container
'
,
()
=>
{
expect
(
buttonEl
).
toBeDefined
();
});
describe
(
'
button
'
,
()
=>
{
it
(
'
should set coordinate
'
,
()
=>
{
expect
(
buttonEl
.
style
.
left
).
toEqual
(
`
${
coordinate
.
x
}
px`
);
expect
(
buttonEl
.
style
.
top
).
toEqual
(
`
${
coordinate
.
y
}
px`
);
});
it
(
'
should contain image-comment-dark svg
'
,
()
=>
{
const
svgEl
=
buttonEl
.
querySelector
(
'
svg
'
);
expect
(
svgEl
).
toBeDefined
();
const
svgLink
=
svgEl
.
querySelector
(
'
use
'
).
getAttribute
(
'
xlink:href
'
);
expect
(
svgLink
.
indexOf
(
'
image-comment-dark
'
)
!==
-
1
).
toEqual
(
true
);
});
});
});
describe
(
'
removeCommentIndicator
'
,
()
=>
{
it
(
'
should return removed false if there is no comment-indicator
'
,
()
=>
{
const
result
=
commentIndicatorHelper
.
removeCommentIndicator
(
containerEl
);
expect
(
result
.
removed
).
toEqual
(
false
);
});
describe
(
'
has comment indicator
'
,
()
=>
{
let
result
;
beforeEach
(()
=>
{
containerEl
.
innerHTML
=
`
<div class="comment-indicator" style="left:
${
coordinate
.
x
}
px; top:
${
coordinate
.
y
}
px;">
<img src="
${
gl
.
TEST_HOST
}
/image.png">
</div>
`
;
result
=
commentIndicatorHelper
.
removeCommentIndicator
(
containerEl
);
});
it
(
'
should remove comment indicator
'
,
()
=>
{
expect
(
containerEl
.
querySelector
(
'
.comment-indicator
'
)).
toBeNull
();
});
it
(
'
should return removed true
'
,
()
=>
{
expect
(
result
.
removed
).
toEqual
(
true
);
});
it
(
'
should return indicator meta
'
,
()
=>
{
expect
(
result
.
x
).
toEqual
(
coordinate
.
x
);
expect
(
result
.
y
).
toEqual
(
coordinate
.
y
);
expect
(
result
.
image
).
toBeDefined
();
expect
(
result
.
image
.
width
).
toBeDefined
();
expect
(
result
.
image
.
height
).
toBeDefined
();
});
});
});
describe
(
'
showCommentIndicator
'
,
()
=>
{
describe
(
'
commentIndicator exists
'
,
()
=>
{
beforeEach
(()
=>
{
containerEl
.
innerHTML
=
`
<button class="comment-indicator"></button>
`
;
commentIndicatorHelper
.
showCommentIndicator
(
containerEl
,
coordinate
);
});
it
(
'
should set commentIndicator coordinates
'
,
()
=>
{
const
commentIndicatorEl
=
containerEl
.
querySelector
(
'
.comment-indicator
'
);
expect
(
commentIndicatorEl
.
style
.
left
).
toEqual
(
`
${
coordinate
.
x
}
px`
);
expect
(
commentIndicatorEl
.
style
.
top
).
toEqual
(
`
${
coordinate
.
y
}
px`
);
});
});
describe
(
'
commentIndicator does not exist
'
,
()
=>
{
beforeEach
(()
=>
{
commentIndicatorHelper
.
showCommentIndicator
(
containerEl
,
coordinate
);
});
it
(
'
should addCommentIndicator
'
,
()
=>
{
const
buttonEl
=
containerEl
.
querySelector
(
'
.comment-indicator
'
);
expect
(
buttonEl
).
toBeDefined
();
expect
(
buttonEl
.
style
.
left
).
toEqual
(
`
${
coordinate
.
x
}
px`
);
expect
(
buttonEl
.
style
.
top
).
toEqual
(
`
${
coordinate
.
y
}
px`
);
});
});
});
describe
(
'
commentIndicatorOnClick
'
,
()
=>
{
let
event
;
let
textAreaEl
;
beforeEach
(()
=>
{
containerEl
.
innerHTML
=
`
<div class="diff-viewer">
<button></button>
<div class="note-container">
<textarea class="note-textarea"></textarea>
</div>
</div>
`
;
textAreaEl
=
containerEl
.
querySelector
(
'
textarea
'
);
event
=
{
stopPropagation
:
()
=>
{},
currentTarget
:
containerEl
.
querySelector
(
'
button
'
),
};
spyOn
(
event
,
'
stopPropagation
'
);
spyOn
(
textAreaEl
,
'
focus
'
);
commentIndicatorHelper
.
commentIndicatorOnClick
(
event
);
});
it
(
'
should stopPropagation
'
,
()
=>
{
expect
(
event
.
stopPropagation
).
toHaveBeenCalled
();
});
it
(
'
should focus textAreaEl
'
,
()
=>
{
expect
(
textAreaEl
.
focus
).
toHaveBeenCalled
();
});
});
});
spec/javascripts/image_diff/helpers/dom_helper_spec.js
0 → 100644
View file @
b54203f0
import
*
as
domHelper
from
'
~/image_diff/helpers/dom_helper
'
;
import
*
as
mockData
from
'
../mock_data
'
;
describe
(
'
domHelper
'
,
()
=>
{
const
{
imageMeta
,
badgeNumber
}
=
mockData
;
describe
(
'
setPositionDataAttribute
'
,
()
=>
{
let
containerEl
;
let
attributeAfterCall
;
const
position
=
{
myProperty
:
'
myProperty
'
,
};
beforeEach
(()
=>
{
containerEl
=
document
.
createElement
(
'
div
'
);
containerEl
.
dataset
.
position
=
JSON
.
stringify
(
position
);
domHelper
.
setPositionDataAttribute
(
containerEl
,
imageMeta
);
attributeAfterCall
=
JSON
.
parse
(
containerEl
.
dataset
.
position
);
});
it
(
'
should set x, y, width, height
'
,
()
=>
{
expect
(
attributeAfterCall
.
x
).
toEqual
(
imageMeta
.
x
);
expect
(
attributeAfterCall
.
y
).
toEqual
(
imageMeta
.
y
);
expect
(
attributeAfterCall
.
width
).
toEqual
(
imageMeta
.
width
);
expect
(
attributeAfterCall
.
height
).
toEqual
(
imageMeta
.
height
);
});
it
(
'
should not override other properties
'
,
()
=>
{
expect
(
attributeAfterCall
.
myProperty
).
toEqual
(
'
myProperty
'
);
});
});
describe
(
'
updateDiscussionAvatarBadgeNumber
'
,
()
=>
{
let
discussionEl
;
beforeEach
(()
=>
{
discussionEl
=
document
.
createElement
(
'
div
'
);
discussionEl
.
innerHTML
=
`
<a href="#" class="image-diff-avatar-link">
<div class="badge"></div>
</a>
`
;
domHelper
.
updateDiscussionAvatarBadgeNumber
(
discussionEl
,
badgeNumber
);
});
it
(
'
should update avatar badge number
'
,
()
=>
{
expect
(
discussionEl
.
querySelector
(
'
.badge
'
).
innerText
).
toEqual
(
badgeNumber
.
toString
());
});
});
describe
(
'
updateDiscussionBadgeNumber
'
,
()
=>
{
let
discussionEl
;
beforeEach
(()
=>
{
discussionEl
=
document
.
createElement
(
'
div
'
);
discussionEl
.
innerHTML
=
`
<div class="badge"></div>
`
;
domHelper
.
updateDiscussionBadgeNumber
(
discussionEl
,
badgeNumber
);
});
it
(
'
should update discussion badge number
'
,
()
=>
{
expect
(
discussionEl
.
querySelector
(
'
.badge
'
).
innerText
).
toEqual
(
badgeNumber
.
toString
());
});
});
describe
(
'
toggleCollapsed
'
,
()
=>
{
let
element
;
let
discussionNotesEl
;
beforeEach
(()
=>
{
element
=
document
.
createElement
(
'
div
'
);
element
.
innerHTML
=
`
<div class="discussion-notes">
<button></button>
<form class="discussion-form"></form>
</div>
`
;
discussionNotesEl
=
element
.
querySelector
(
'
.discussion-notes
'
);
});
describe
(
'
not collapsed
'
,
()
=>
{
beforeEach
(()
=>
{
domHelper
.
toggleCollapsed
({
currentTarget
:
element
.
querySelector
(
'
button
'
),
});
});
it
(
'
should add collapsed class
'
,
()
=>
{
expect
(
discussionNotesEl
.
classList
.
contains
(
'
collapsed
'
)).
toEqual
(
true
);
});
it
(
'
should force formEl to display none
'
,
()
=>
{
const
formEl
=
element
.
querySelector
(
'
.discussion-form
'
);
expect
(
formEl
.
style
.
display
).
toEqual
(
'
none
'
);
});
});
describe
(
'
collapsed
'
,
()
=>
{
beforeEach
(()
=>
{
discussionNotesEl
.
classList
.
add
(
'
collapsed
'
);
domHelper
.
toggleCollapsed
({
currentTarget
:
element
.
querySelector
(
'
button
'
),
});
});
it
(
'
should remove collapsed class
'
,
()
=>
{
expect
(
discussionNotesEl
.
classList
.
contains
(
'
collapsed
'
)).
toEqual
(
false
);
});
it
(
'
should force formEl to display block
'
,
()
=>
{
const
formEl
=
element
.
querySelector
(
'
.discussion-form
'
);
expect
(
formEl
.
style
.
display
).
toEqual
(
'
block
'
);
});
});
});
});
spec/javascripts/image_diff/helpers/utils_helper_spec.js
0 → 100644
View file @
b54203f0
import
*
as
utilsHelper
from
'
~/image_diff/helpers/utils_helper
'
;
import
ImageDiff
from
'
~/image_diff/image_diff
'
;
import
ReplacedImageDiff
from
'
~/image_diff/replaced_image_diff
'
;
import
ImageBadge
from
'
~/image_diff/image_badge
'
;
import
*
as
mockData
from
'
../mock_data
'
;
describe
(
'
utilsHelper
'
,
()
=>
{
const
{
noteId
,
discussionId
,
image
,
imageProperties
,
imageMeta
,
}
=
mockData
;
describe
(
'
resizeCoordinatesToImageElement
'
,
()
=>
{
let
result
;
beforeEach
(()
=>
{
result
=
utilsHelper
.
resizeCoordinatesToImageElement
(
image
,
imageMeta
);
});
it
(
'
should return x based on widthRatio
'
,
()
=>
{
expect
(
result
.
x
).
toEqual
(
imageMeta
.
x
*
0.5
);
});
it
(
'
should return y based on heightRatio
'
,
()
=>
{
expect
(
result
.
y
).
toEqual
(
imageMeta
.
y
*
0.5
);
});
it
(
'
should return image width
'
,
()
=>
{
expect
(
result
.
width
).
toEqual
(
image
.
width
);
});
it
(
'
should return image height
'
,
()
=>
{
expect
(
result
.
height
).
toEqual
(
image
.
height
);
});
});
describe
(
'
generateBadgeFromDiscussionDOM
'
,
()
=>
{
let
discussionEl
;
let
result
;
beforeEach
(()
=>
{
const
imageFrameEl
=
document
.
createElement
(
'
div
'
);
imageFrameEl
.
innerHTML
=
`
<img src="
${
gl
.
TEST_HOST
}
/image.png">
`
;
discussionEl
=
document
.
createElement
(
'
div
'
);
discussionEl
.
dataset
.
discussionId
=
discussionId
;
discussionEl
.
innerHTML
=
`
<div class="note" id="
${
noteId
}
"></div>
`
;
discussionEl
.
dataset
.
position
=
JSON
.
stringify
(
imageMeta
);
result
=
utilsHelper
.
generateBadgeFromDiscussionDOM
(
imageFrameEl
,
discussionEl
);
});
it
(
'
should return actual image properties
'
,
()
=>
{
const
{
actual
}
=
result
;
expect
(
actual
.
x
).
toEqual
(
imageMeta
.
x
);
expect
(
actual
.
y
).
toEqual
(
imageMeta
.
y
);
expect
(
actual
.
width
).
toEqual
(
imageMeta
.
width
);
expect
(
actual
.
height
).
toEqual
(
imageMeta
.
height
);
});
it
(
'
should return browser image properties
'
,
()
=>
{
const
{
browser
}
=
result
;
expect
(
browser
.
x
).
toBeDefined
();
expect
(
browser
.
y
).
toBeDefined
();
expect
(
browser
.
width
).
toBeDefined
();
expect
(
browser
.
height
).
toBeDefined
();
});
it
(
'
should return instance of ImageBadge
'
,
()
=>
{
expect
(
result
instanceof
ImageBadge
).
toEqual
(
true
);
});
it
(
'
should return noteId
'
,
()
=>
{
expect
(
result
.
noteId
).
toEqual
(
noteId
);
});
it
(
'
should return discussionId
'
,
()
=>
{
expect
(
result
.
discussionId
).
toEqual
(
discussionId
);
});
});
describe
(
'
getTargetSelection
'
,
()
=>
{
let
containerEl
;
beforeEach
(()
=>
{
containerEl
=
{
querySelector
:
()
=>
imageProperties
,
};
});
function
generateEvent
(
offsetX
,
offsetY
)
{
return
{
currentTarget
:
containerEl
,
offsetX
,
offsetY
,
};
}
it
(
'
should return browser properties
'
,
()
=>
{
const
event
=
generateEvent
(
25
,
25
);
const
result
=
utilsHelper
.
getTargetSelection
(
event
);
const
{
browser
}
=
result
;
expect
(
browser
.
x
).
toEqual
(
event
.
offsetX
);
expect
(
browser
.
y
).
toEqual
(
event
.
offsetY
);
expect
(
browser
.
width
).
toEqual
(
imageProperties
.
width
);
expect
(
browser
.
height
).
toEqual
(
imageProperties
.
height
);
});
it
(
'
should return resized actual image properties
'
,
()
=>
{
const
event
=
generateEvent
(
50
,
50
);
const
result
=
utilsHelper
.
getTargetSelection
(
event
);
const
{
actual
}
=
result
;
expect
(
actual
.
x
).
toEqual
(
100
);
expect
(
actual
.
y
).
toEqual
(
100
);
expect
(
actual
.
width
).
toEqual
(
imageProperties
.
naturalWidth
);
expect
(
actual
.
height
).
toEqual
(
imageProperties
.
naturalHeight
);
});
describe
(
'
normalize coordinates
'
,
()
=>
{
it
(
'
should return x = 0 if x < 0
'
,
()
=>
{
const
event
=
generateEvent
(
-
5
,
50
);
const
result
=
utilsHelper
.
getTargetSelection
(
event
);
expect
(
result
.
browser
.
x
).
toEqual
(
0
);
});
it
(
'
should return x = width if x > width
'
,
()
=>
{
const
event
=
generateEvent
(
1000
,
50
);
const
result
=
utilsHelper
.
getTargetSelection
(
event
);
expect
(
result
.
browser
.
x
).
toEqual
(
imageProperties
.
width
);
});
it
(
'
should return y = 0 if y < 0
'
,
()
=>
{
const
event
=
generateEvent
(
50
,
-
10
);
const
result
=
utilsHelper
.
getTargetSelection
(
event
);
expect
(
result
.
browser
.
y
).
toEqual
(
0
);
});
it
(
'
should return y = height if y > height
'
,
()
=>
{
const
event
=
generateEvent
(
50
,
1000
);
const
result
=
utilsHelper
.
getTargetSelection
(
event
);
expect
(
result
.
browser
.
y
).
toEqual
(
imageProperties
.
height
);
});
});
});
describe
(
'
initImageDiff
'
,
()
=>
{
let
glCache
;
let
fileEl
;
beforeEach
(()
=>
{
window
.
gl
=
window
.
gl
||
(
window
.
gl
=
{});
glCache
=
window
.
gl
;
window
.
gl
.
ImageFile
=
()
=>
{};
fileEl
=
document
.
createElement
(
'
div
'
);
fileEl
.
innerHTML
=
`
<div class="diff-file"></div>
`
;
spyOn
(
ImageDiff
.
prototype
,
'
init
'
).
and
.
callFake
(()
=>
{});
spyOn
(
ReplacedImageDiff
.
prototype
,
'
init
'
).
and
.
callFake
(()
=>
{});
});
afterEach
(()
=>
{
window
.
gl
=
glCache
;
});
it
(
'
should initialize gl.ImageFile
'
,
()
=>
{
spyOn
(
window
.
gl
,
'
ImageFile
'
);
utilsHelper
.
initImageDiff
(
fileEl
,
false
,
false
);
expect
(
gl
.
ImageFile
).
toHaveBeenCalled
();
});
it
(
'
should initialize ImageDiff if js-single-image
'
,
()
=>
{
const
diffFileEl
=
fileEl
.
querySelector
(
'
.diff-file
'
);
diffFileEl
.
innerHTML
=
`
<div class="js-single-image">
</div>
`
;
const
imageDiff
=
utilsHelper
.
initImageDiff
(
fileEl
,
true
,
false
);
expect
(
ImageDiff
.
prototype
.
init
).
toHaveBeenCalled
();
expect
(
imageDiff
.
canCreateNote
).
toEqual
(
true
);
expect
(
imageDiff
.
renderCommentBadge
).
toEqual
(
false
);
});
it
(
'
should initialize ReplacedImageDiff if js-replaced-image
'
,
()
=>
{
const
diffFileEl
=
fileEl
.
querySelector
(
'
.diff-file
'
);
diffFileEl
.
innerHTML
=
`
<div class="js-replaced-image">
</div>
`
;
const
replacedImageDiff
=
utilsHelper
.
initImageDiff
(
fileEl
,
false
,
true
);
expect
(
ReplacedImageDiff
.
prototype
.
init
).
toHaveBeenCalled
();
expect
(
replacedImageDiff
.
canCreateNote
).
toEqual
(
false
);
expect
(
replacedImageDiff
.
renderCommentBadge
).
toEqual
(
true
);
});
});
});
spec/javascripts/image_diff/image_badge_spec.js
0 → 100644
View file @
b54203f0
import
ImageBadge
from
'
~/image_diff/image_badge
'
;
import
imageDiffHelper
from
'
~/image_diff/helpers/index
'
;
import
*
as
mockData
from
'
./mock_data
'
;
describe
(
'
ImageBadge
'
,
()
=>
{
const
{
noteId
,
discussionId
,
imageMeta
}
=
mockData
;
const
options
=
{
noteId
,
discussionId
,
};
it
(
'
should save actual property
'
,
()
=>
{
const
imageBadge
=
new
ImageBadge
(
Object
.
assign
({},
options
,
{
actual
:
imageMeta
,
}));
const
{
actual
}
=
imageBadge
;
expect
(
actual
.
x
).
toEqual
(
imageMeta
.
x
);
expect
(
actual
.
y
).
toEqual
(
imageMeta
.
y
);
expect
(
actual
.
width
).
toEqual
(
imageMeta
.
width
);
expect
(
actual
.
height
).
toEqual
(
imageMeta
.
height
);
});
it
(
'
should save browser property
'
,
()
=>
{
const
imageBadge
=
new
ImageBadge
(
Object
.
assign
({},
options
,
{
browser
:
imageMeta
,
}));
const
{
browser
}
=
imageBadge
;
expect
(
browser
.
x
).
toEqual
(
imageMeta
.
x
);
expect
(
browser
.
y
).
toEqual
(
imageMeta
.
y
);
expect
(
browser
.
width
).
toEqual
(
imageMeta
.
width
);
expect
(
browser
.
height
).
toEqual
(
imageMeta
.
height
);
});
it
(
'
should save noteId
'
,
()
=>
{
const
imageBadge
=
new
ImageBadge
(
options
);
expect
(
imageBadge
.
noteId
).
toEqual
(
noteId
);
});
it
(
'
should save discussionId
'
,
()
=>
{
const
imageBadge
=
new
ImageBadge
(
options
);
expect
(
imageBadge
.
discussionId
).
toEqual
(
discussionId
);
});
describe
(
'
default values
'
,
()
=>
{
let
imageBadge
;
beforeEach
(()
=>
{
imageBadge
=
new
ImageBadge
(
options
);
});
it
(
'
should return defaultimageMeta if actual property is not provided
'
,
()
=>
{
const
{
actual
}
=
imageBadge
;
expect
(
actual
.
x
).
toEqual
(
0
);
expect
(
actual
.
y
).
toEqual
(
0
);
expect
(
actual
.
width
).
toEqual
(
0
);
expect
(
actual
.
height
).
toEqual
(
0
);
});
it
(
'
should return defaultimageMeta if browser property is not provided
'
,
()
=>
{
const
{
browser
}
=
imageBadge
;
expect
(
browser
.
x
).
toEqual
(
0
);
expect
(
browser
.
y
).
toEqual
(
0
);
expect
(
browser
.
width
).
toEqual
(
0
);
expect
(
browser
.
height
).
toEqual
(
0
);
});
});
describe
(
'
imageEl property is provided and not browser property
'
,
()
=>
{
beforeEach
(()
=>
{
spyOn
(
imageDiffHelper
,
'
resizeCoordinatesToImageElement
'
).
and
.
returnValue
(
true
);
});
it
(
'
should generate browser property
'
,
()
=>
{
const
imageBadge
=
new
ImageBadge
(
Object
.
assign
({},
options
,
{
imageEl
:
document
.
createElement
(
'
img
'
),
}));
expect
(
imageDiffHelper
.
resizeCoordinatesToImageElement
).
toHaveBeenCalled
();
expect
(
imageBadge
.
browser
).
toEqual
(
true
);
});
});
});
spec/javascripts/image_diff/image_diff_spec.js
0 → 100644
View file @
b54203f0
This diff is collapsed.
Click to expand it.
spec/javascripts/image_diff/init_discussion_tab_spec.js
0 → 100644
View file @
b54203f0
import
initDiscussionTab
from
'
~/image_diff/init_discussion_tab
'
;
import
imageDiffHelper
from
'
~/image_diff/helpers/index
'
;
describe
(
'
initDiscussionTab
'
,
()
=>
{
beforeEach
(()
=>
{
setFixtures
(
`
<div class="timeline-content">
<div class="diff-file js-image-file"></div>
<div class="diff-file js-image-file"></div>
</div>
`
);
});
it
(
'
should pass canCreateNote as false to initImageDiff
'
,
(
done
)
=>
{
spyOn
(
imageDiffHelper
,
'
initImageDiff
'
).
and
.
callFake
((
diffFileEl
,
canCreateNote
)
=>
{
expect
(
canCreateNote
).
toEqual
(
false
);
done
();
});
initDiscussionTab
();
});
it
(
'
should pass renderCommentBadge as true to initImageDiff
'
,
(
done
)
=>
{
spyOn
(
imageDiffHelper
,
'
initImageDiff
'
).
and
.
callFake
((
diffFileEl
,
canCreateNote
,
renderCommentBadge
)
=>
{
expect
(
renderCommentBadge
).
toEqual
(
true
);
done
();
});
initDiscussionTab
();
});
it
(
'
should call initImageDiff for each diffFileEls
'
,
()
=>
{
spyOn
(
imageDiffHelper
,
'
initImageDiff
'
).
and
.
callFake
(()
=>
{});
initDiscussionTab
();
expect
(
imageDiffHelper
.
initImageDiff
.
calls
.
count
()).
toEqual
(
2
);
});
});
spec/javascripts/image_diff/mock_data.js
0 → 100644
View file @
b54203f0
export
const
noteId
=
'
noteId
'
;
export
const
discussionId
=
'
discussionId
'
;
export
const
badgeText
=
'
badgeText
'
;
export
const
badgeNumber
=
5
;
export
const
coordinate
=
{
x
:
100
,
y
:
100
,
};
export
const
image
=
{
width
:
100
,
height
:
100
,
};
export
const
imageProperties
=
{
width
:
image
.
width
,
height
:
image
.
height
,
naturalWidth
:
image
.
width
*
2
,
naturalHeight
:
image
.
height
*
2
,
};
export
const
imageMeta
=
{
x
:
coordinate
.
x
,
y
:
coordinate
.
y
,
width
:
imageProperties
.
naturalWidth
,
height
:
imageProperties
.
naturalHeight
,
};
spec/javascripts/image_diff/replaced_image_diff_spec.js
0 → 100644
View file @
b54203f0
This diff is collapsed.
Click to expand it.
spec/javascripts/image_diff/view_types_spec.js
0 → 100644
View file @
b54203f0
import
{
viewTypes
,
isValidViewType
}
from
'
~/image_diff/view_types
'
;
describe
(
'
viewTypes
'
,
()
=>
{
describe
(
'
isValidViewType
'
,
()
=>
{
it
(
'
should return true for TWO_UP
'
,
()
=>
{
expect
(
isValidViewType
(
viewTypes
.
TWO_UP
)).
toEqual
(
true
);
});
it
(
'
should return true for SWIPE
'
,
()
=>
{
expect
(
isValidViewType
(
viewTypes
.
SWIPE
)).
toEqual
(
true
);
});
it
(
'
should return true for ONION_SKIN
'
,
()
=>
{
expect
(
isValidViewType
(
viewTypes
.
ONION_SKIN
)).
toEqual
(
true
);
});
it
(
'
should return false for non view types
'
,
()
=>
{
expect
(
isValidViewType
(
'
some-view-type
'
)).
toEqual
(
false
);
expect
(
isValidViewType
(
null
)).
toEqual
(
false
);
expect
(
isValidViewType
(
undefined
)).
toEqual
(
false
);
expect
(
isValidViewType
(
''
)).
toEqual
(
false
);
});
});
});
spec/javascripts/lib/utils/image_utility_spec.js
0 → 100644
View file @
b54203f0
import
*
as
imageUtility
from
'
~/lib/utils/image_utility
'
;
describe
(
'
imageUtility
'
,
()
=>
{
describe
(
'
isImageLoaded
'
,
()
=>
{
it
(
'
should return false when image.complete is false
'
,
()
=>
{
const
element
=
{
complete
:
false
,
naturalHeight
:
100
,
};
expect
(
imageUtility
.
isImageLoaded
(
element
)).
toEqual
(
false
);
});
it
(
'
should return false when naturalHeight = 0
'
,
()
=>
{
const
element
=
{
complete
:
true
,
naturalHeight
:
0
,
};
expect
(
imageUtility
.
isImageLoaded
(
element
)).
toEqual
(
false
);
});
it
(
'
should return true when image.complete and naturalHeight != 0
'
,
()
=>
{
const
element
=
{
complete
:
true
,
naturalHeight
:
100
,
};
expect
(
imageUtility
.
isImageLoaded
(
element
)).
toEqual
(
true
);
});
});
});
spec/lib/gitlab/diff/formatters/image_formatter_spec.rb
0 → 100644
View file @
b54203f0
require
'spec_helper'
describe
Gitlab
::
Diff
::
Formatters
::
ImageFormatter
do
it_behaves_like
"position formatter"
do
let
(
:base_attrs
)
do
{
base_sha:
123
,
start_sha:
456
,
head_sha:
789
,
old_path:
'old_image.png'
,
new_path:
'new_image.png'
,
position_type:
'image'
}
end
let
(
:attrs
)
do
base_attrs
.
merge
(
width:
100
,
height:
100
,
x:
1
,
y:
2
)
end
end
end
spec/lib/gitlab/diff/formatters/text_formatter_spec.rb
0 → 100644
View file @
b54203f0
require
'spec_helper'
describe
Gitlab
::
Diff
::
Formatters
::
TextFormatter
do
let!
(
:base
)
do
{
base_sha:
123
,
start_sha:
456
,
head_sha:
789
,
old_path:
'old_path.txt'
,
new_path:
'new_path.txt'
}
end
let!
(
:complete
)
do
base
.
merge
(
old_line:
1
,
new_line:
2
)
end
it_behaves_like
"position formatter"
do
let
(
:base_attrs
)
{
base
}
let
(
:attrs
)
{
complete
}
end
# Specific text formatter examples
let!
(
:formatter
)
{
described_class
.
new
(
attrs
)
}
describe
'#line_age'
do
subject
{
formatter
.
line_age
}
context
' when there is only new_line'
do
let
(
:attrs
)
{
base
.
merge
(
new_line:
1
)
}
it
{
is_expected
.
to
eq
(
'new'
)
}
end
context
' when there is only old_line'
do
let
(
:attrs
)
{
base
.
merge
(
old_line:
1
)
}
it
{
is_expected
.
to
eq
(
'old'
)
}
end
end
end
spec/lib/gitlab/diff/position_spec.rb
View file @
b54203f0
...
@@ -5,7 +5,7 @@ describe Gitlab::Diff::Position do
...
@@ -5,7 +5,7 @@ describe Gitlab::Diff::Position do
let
(
:project
)
{
create
(
:project
,
:repository
)
}
let
(
:project
)
{
create
(
:project
,
:repository
)
}
describe
"position for an added file"
do
describe
"position for an added
text
file"
do
let
(
:commit
)
{
project
.
commit
(
"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
)
}
let
(
:commit
)
{
project
.
commit
(
"2ea1f3dec713d940208fb5ce4a38765ecb5d3f73"
)
}
subject
do
subject
do
...
@@ -47,6 +47,31 @@ describe Gitlab::Diff::Position do
...
@@ -47,6 +47,31 @@ describe Gitlab::Diff::Position do
end
end
end
end
describe
"position for an added image file"
do
let
(
:commit
)
{
project
.
commit
(
"33f3729a45c02fc67d00adb1b8bca394b0e761d9"
)
}
subject
do
described_class
.
new
(
old_path:
"files/images/6049019_460s.jpg"
,
new_path:
"files/images/6049019_460s.jpg"
,
width:
100
,
height:
100
,
x:
1
,
y:
100
,
diff_refs:
commit
.
diff_refs
,
position_type:
"image"
)
end
it
"returns the correct diff file"
do
diff_file
=
subject
.
diff_file
(
project
.
repository
)
expect
(
diff_file
.
new_file?
).
to
be
true
expect
(
diff_file
.
new_path
).
to
eq
(
subject
.
new_path
)
expect
(
diff_file
.
diff_refs
).
to
eq
(
subject
.
diff_refs
)
end
end
describe
"position for a changed file"
do
describe
"position for a changed file"
do
let
(
:commit
)
{
project
.
commit
(
"570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
)
}
let
(
:commit
)
{
project
.
commit
(
"570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
)
}
...
@@ -468,26 +493,54 @@ describe Gitlab::Diff::Position do
...
@@ -468,26 +493,54 @@ describe Gitlab::Diff::Position do
end
end
describe
"#to_json"
do
describe
"#to_json"
do
let
(
:hash
)
do
shared_examples
"diff position json"
do
{
it
"returns the position as JSON"
do
old_path:
"files/ruby/popen.rb"
,
expect
(
JSON
.
parse
(
diff_position
.
to_json
)).
to
eq
(
hash
.
stringify_keys
)
new_path:
"files/ruby/popen.rb"
,
end
old_line:
nil
,
new_line:
14
,
it
"works when nested under another hash"
do
base_sha:
nil
,
expect
(
JSON
.
parse
(
JSON
.
generate
(
pos:
diff_position
))).
to
eq
(
'pos'
=>
hash
.
stringify_keys
)
head_sha:
nil
,
end
start_sha:
nil
}
end
end
let
(
:diff_position
)
{
described_class
.
new
(
hash
)
}
context
"for text positon"
do
let
(
:hash
)
do
{
old_path:
"files/ruby/popen.rb"
,
new_path:
"files/ruby/popen.rb"
,
old_line:
nil
,
new_line:
14
,
base_sha:
nil
,
head_sha:
nil
,
start_sha:
nil
,
position_type:
"text"
}
end
let
(
:diff_position
)
{
described_class
.
new
(
hash
)
}
it
"returns the position as JSON"
do
it_behaves_like
"diff position json"
expect
(
JSON
.
parse
(
diff_position
.
to_json
)).
to
eq
(
hash
.
stringify_keys
)
end
end
it
"works when nested under another hash"
do
context
"for image positon"
do
expect
(
JSON
.
parse
(
JSON
.
generate
(
pos:
diff_position
))).
to
eq
(
'pos'
=>
hash
.
stringify_keys
)
let
(
:hash
)
do
{
old_path:
"files/any.img"
,
new_path:
"files/any.img"
,
base_sha:
nil
,
head_sha:
nil
,
start_sha:
nil
,
width:
100
,
height:
100
,
x:
1
,
y:
100
,
position_type:
"image"
}
end
let
(
:diff_position
)
{
described_class
.
new
(
hash
)
}
it_behaves_like
"diff position json"
end
end
end
end
end
end
spec/lib/gitlab/diff/position_tracer_spec.rb
View file @
b54203f0
...
@@ -71,6 +71,10 @@ describe Gitlab::Diff::PositionTracer do
...
@@ -71,6 +71,10 @@ describe Gitlab::Diff::PositionTracer do
Gitlab
::
Diff
::
DiffRefs
.
new
(
base_sha:
base_commit
.
id
,
head_sha:
head_commit
.
id
)
Gitlab
::
Diff
::
DiffRefs
.
new
(
base_sha:
base_commit
.
id
,
head_sha:
head_commit
.
id
)
end
end
def
text_position_attrs
[
:old_line
,
:new_line
]
end
def
position
(
attrs
=
{})
def
position
(
attrs
=
{})
attrs
.
reverse_merge!
(
attrs
.
reverse_merge!
(
diff_refs:
old_diff_refs
diff_refs:
old_diff_refs
...
@@ -91,7 +95,11 @@ describe Gitlab::Diff::PositionTracer do
...
@@ -91,7 +95,11 @@ describe Gitlab::Diff::PositionTracer do
expect
(
new_position
.
diff_refs
).
to
eq
(
new_diff_refs
)
expect
(
new_position
.
diff_refs
).
to
eq
(
new_diff_refs
)
attrs
.
each
do
|
attr
,
value
|
attrs
.
each
do
|
attr
,
value
|
expect
(
new_position
.
send
(
attr
)).
to
eq
(
value
)
if
text_position_attrs
.
include?
(
attr
)
expect
(
new_position
.
formatter
.
send
(
attr
)).
to
eq
(
value
)
else
expect
(
new_position
.
send
(
attr
)).
to
eq
(
value
)
end
end
end
end
end
end
end
...
@@ -110,7 +118,11 @@ describe Gitlab::Diff::PositionTracer do
...
@@ -110,7 +118,11 @@ describe Gitlab::Diff::PositionTracer do
expect
(
change_position
.
diff_refs
).
to
eq
(
change_diff_refs
)
expect
(
change_position
.
diff_refs
).
to
eq
(
change_diff_refs
)
attrs
.
each
do
|
attr
,
value
|
attrs
.
each
do
|
attr
,
value
|
expect
(
change_position
.
send
(
attr
)).
to
eq
(
value
)
if
text_position_attrs
.
include?
(
attr
)
expect
(
change_position
.
formatter
.
send
(
attr
)).
to
eq
(
value
)
else
expect
(
change_position
.
send
(
attr
)).
to
eq
(
value
)
end
end
end
end
end
end
end
...
...
spec/models/diff_note_spec.rb
View file @
b54203f0
...
@@ -3,7 +3,7 @@ require 'spec_helper'
...
@@ -3,7 +3,7 @@ require 'spec_helper'
describe
DiffNote
do
describe
DiffNote
do
include
RepoHelpers
include
RepoHelpers
let
(
:merge_request
)
{
create
(
:merge_request
)
}
let
!
(
:merge_request
)
{
create
(
:merge_request
)
}
let
(
:project
)
{
merge_request
.
project
}
let
(
:project
)
{
merge_request
.
project
}
let
(
:commit
)
{
project
.
commit
(
sample_commit
.
id
)
}
let
(
:commit
)
{
project
.
commit
(
sample_commit
.
id
)
}
...
@@ -98,14 +98,14 @@ describe DiffNote do
...
@@ -98,14 +98,14 @@ describe DiffNote do
diff_line
=
subject
.
diff_line
diff_line
=
subject
.
diff_line
expect
(
diff_line
.
added?
).
to
be
true
expect
(
diff_line
.
added?
).
to
be
true
expect
(
diff_line
.
new_line
).
to
eq
(
position
.
new_line
)
expect
(
diff_line
.
new_line
).
to
eq
(
position
.
formatter
.
new_line
)
expect
(
diff_line
.
text
).
to
eq
(
"+ vars = {"
)
expect
(
diff_line
.
text
).
to
eq
(
"+ vars = {"
)
end
end
end
end
describe
"#line_code"
do
describe
"#line_code"
do
it
"returns the correct line code"
do
it
"returns the correct line code"
do
line_code
=
Gitlab
::
Diff
::
LineCode
.
generate
(
position
.
file_path
,
position
.
new_line
,
15
)
line_code
=
Gitlab
::
Diff
::
LineCode
.
generate
(
position
.
file_path
,
position
.
formatter
.
new_line
,
15
)
expect
(
subject
.
line_code
).
to
eq
(
line_code
)
expect
(
subject
.
line_code
).
to
eq
(
line_code
)
end
end
...
@@ -255,4 +255,38 @@ describe DiffNote do
...
@@ -255,4 +255,38 @@ describe DiffNote do
end
end
end
end
end
end
describe
"image diff notes"
do
let
(
:path
)
{
"files/images/any_image.png"
}
let!
(
:position
)
do
Gitlab
::
Diff
::
Position
.
new
(
old_path:
path
,
new_path:
path
,
width:
10
,
height:
10
,
x:
1
,
y:
1
,
diff_refs:
merge_request
.
diff_refs
,
position_type:
"image"
)
end
describe
"validations"
do
subject
{
build
(
:diff_note_on_merge_request
,
project:
project
,
position:
position
,
noteable:
merge_request
)
}
it
{
is_expected
.
not_to
validate_presence_of
(
:line_code
)
}
it
"does not validate diff line"
do
diff_line
=
subject
.
diff_line
expect
(
diff_line
).
to
be
nil
expect
(
subject
).
to
be_valid
end
end
it
"returns true for on_image?"
do
expect
(
subject
.
on_image?
).
to
be_truthy
end
end
end
end
spec/models/note_spec.rb
View file @
b54203f0
...
@@ -314,6 +314,56 @@ describe Note do
...
@@ -314,6 +314,56 @@ describe Note do
expect
(
subject
[
active_diff_note1
.
line_code
].
first
.
id
).
to
eq
(
active_diff_note1
.
discussion_id
)
expect
(
subject
[
active_diff_note1
.
line_code
].
first
.
id
).
to
eq
(
active_diff_note1
.
discussion_id
)
expect
(
subject
[
active_diff_note3
.
line_code
].
first
.
id
).
to
eq
(
active_diff_note3
.
discussion_id
)
expect
(
subject
[
active_diff_note3
.
line_code
].
first
.
id
).
to
eq
(
active_diff_note3
.
discussion_id
)
end
end
context
'with image discussions'
do
let
(
:merge_request2
)
{
create
(
:merge_request_with_diffs
,
:with_image_diffs
,
source_project:
project
,
title:
"Added images and changes"
)
}
let
(
:image_path
)
{
"files/images/ee_repo_logo.png"
}
let
(
:text_path
)
{
"bar/branch-test.txt"
}
let!
(
:image_note
)
{
create
(
:diff_note_on_merge_request
,
project:
project
,
noteable:
merge_request2
,
position:
image_position
)
}
let!
(
:text_note
)
{
create
(
:diff_note_on_merge_request
,
project:
project
,
noteable:
merge_request2
,
position:
text_position
)
}
let
(
:image_position
)
do
Gitlab
::
Diff
::
Position
.
new
(
old_path:
image_path
,
new_path:
image_path
,
width:
100
,
height:
100
,
x:
1
,
y:
1
,
position_type:
"image"
,
diff_refs:
merge_request2
.
diff_refs
)
end
let
(
:text_position
)
do
Gitlab
::
Diff
::
Position
.
new
(
old_path:
text_path
,
new_path:
text_path
,
old_line:
nil
,
new_line:
2
,
position_type:
"text"
,
diff_refs:
merge_request2
.
diff_refs
)
end
it
"groups image discussions by file identifier"
do
diff_discussion
=
DiffDiscussion
.
new
([
image_note
])
discussions
=
merge_request2
.
notes
.
grouped_diff_discussions
expect
(
discussions
.
size
).
to
eq
(
2
)
expect
(
discussions
[
image_note
.
diff_file
.
new_path
]).
to
include
(
diff_discussion
)
end
it
"groups text discussions by line code"
do
diff_discussion
=
DiffDiscussion
.
new
([
text_note
])
discussions
=
merge_request2
.
notes
.
grouped_diff_discussions
expect
(
discussions
.
size
).
to
eq
(
2
)
expect
(
discussions
[
text_note
.
line_code
]).
to
include
(
diff_discussion
)
end
end
end
end
context
'diff discussions for older diff refs'
do
context
'diff discussions for older diff refs'
do
...
...
spec/services/discussions/update_diff_position_service_spec.rb
View file @
b54203f0
...
@@ -164,8 +164,8 @@ describe Discussions::UpdateDiffPositionService do
...
@@ -164,8 +164,8 @@ describe Discussions::UpdateDiffPositionService do
change_position
=
discussion
.
change_position
change_position
=
discussion
.
change_position
expect
(
change_position
.
start_sha
).
to
eq
(
old_diff_refs
.
head_sha
)
expect
(
change_position
.
start_sha
).
to
eq
(
old_diff_refs
.
head_sha
)
expect
(
change_position
.
head_sha
).
to
eq
(
new_diff_refs
.
head_sha
)
expect
(
change_position
.
head_sha
).
to
eq
(
new_diff_refs
.
head_sha
)
expect
(
change_position
.
old_line
).
to
eq
(
9
)
expect
(
change_position
.
formatter
.
old_line
).
to
eq
(
9
)
expect
(
change_position
.
new_line
).
to
be_nil
expect
(
change_position
.
formatter
.
new_line
).
to
be_nil
end
end
it
'creates a system discussion'
do
it
'creates a system discussion'
do
...
@@ -184,7 +184,7 @@ describe Discussions::UpdateDiffPositionService do
...
@@ -184,7 +184,7 @@ describe Discussions::UpdateDiffPositionService do
expect
(
discussion
.
original_position
).
to
eq
(
old_position
)
expect
(
discussion
.
original_position
).
to
eq
(
old_position
)
expect
(
discussion
.
position
).
not_to
eq
(
old_position
)
expect
(
discussion
.
position
).
not_to
eq
(
old_position
)
expect
(
discussion
.
position
.
new_line
).
to
eq
(
22
)
expect
(
discussion
.
position
.
formatter
.
new_line
).
to
eq
(
22
)
end
end
context
'when the resolve_outdated_diff_discussions setting is set'
do
context
'when the resolve_outdated_diff_discussions setting is set'
do
...
...
spec/support/features/discussion_comments_shared_example.rb
View file @
b54203f0
...
@@ -71,7 +71,7 @@ shared_examples 'discussion comments' do |resource_name|
...
@@ -71,7 +71,7 @@ shared_examples 'discussion comments' do |resource_name|
expect
(
page
).
not_to
have_selector
menu_selector
expect
(
page
).
not_to
have_selector
menu_selector
find
(
toggle_selector
).
click
find
(
toggle_selector
).
click
find
(
'body'
).
click
find
(
'body'
).
trigger
'click'
expect
(
page
).
not_to
have_selector
menu_selector
expect
(
page
).
not_to
have_selector
menu_selector
end
end
...
...
spec/support/shared_examples/position_formatters.rb
0 → 100644
View file @
b54203f0
shared_examples_for
"position formatter"
do
let
(
:formatter
)
{
described_class
.
new
(
attrs
)
}
describe
'#key'
do
let
(
:key
)
{
[
123
,
456
,
789
,
Digest
::
SHA1
.
hexdigest
(
formatter
.
old_path
),
Digest
::
SHA1
.
hexdigest
(
formatter
.
new_path
),
1
,
2
]
}
subject
{
formatter
.
key
}
it
{
is_expected
.
to
eq
(
key
)
}
end
describe
'#complete?'
do
subject
{
formatter
.
complete?
}
context
'when there are missing key attributes'
do
it
{
is_expected
.
to
be_truthy
}
end
context
'when old_line and new_line are nil'
do
let
(
:attrs
)
{
base_attrs
}
it
{
is_expected
.
to
be_falsy
}
end
end
describe
'#to_h'
do
let
(
:formatter_hash
)
do
attrs
.
merge
(
position_type:
base_attrs
[
:position_type
]
||
'text'
)
end
subject
{
formatter
.
to_h
}
it
{
is_expected
.
to
eq
(
formatter_hash
)
}
end
describe
'#=='
do
subject
{
formatter
}
let
(
:other_formatter
)
{
described_class
.
new
(
attrs
)
}
it
{
is_expected
.
to
eq
(
other_formatter
)
}
end
end
spec/support/test_env.rb
View file @
b54203f0
...
@@ -46,7 +46,8 @@ module TestEnv
...
@@ -46,7 +46,8 @@ module TestEnv
'v1.1.0'
=>
'b83d6e3'
,
'v1.1.0'
=>
'b83d6e3'
,
'add-ipython-files'
=>
'93ee732'
,
'add-ipython-files'
=>
'93ee732'
,
'add-pdf-file'
=>
'e774ebd'
,
'add-pdf-file'
=>
'e774ebd'
,
'add-pdf-text-binary'
=>
'79faa7b'
'add-pdf-text-binary'
=>
'79faa7b'
,
'add_images_and_changes'
=>
'010d106'
}.
freeze
}.
freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
...
...
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