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
059ab73b
Commit
059ab73b
authored
Feb 28, 2018
by
Fatih Acet
Committed by
Jacob Schatz
Feb 28, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Render MR Notes with Vue with behind a cookie
parent
0be4a77d
Changes
74
Show whitespace changes
Inline
Side-by-side
Showing
74 changed files
with
1747 additions
and
327 deletions
+1747
-327
app/assets/javascripts/autosave.js
app/assets/javascripts/autosave.js
+14
-18
app/assets/javascripts/awards_handler.js
app/assets/javascripts/awards_handler.js
+12
-4
app/assets/javascripts/diff_notes/components/jump_to_discussion.js
...s/javascripts/diff_notes/components/jump_to_discussion.js
+1
-1
app/assets/javascripts/diff_notes/components/resolve_btn.js
app/assets/javascripts/diff_notes/components/resolve_btn.js
+1
-0
app/assets/javascripts/diff_notes/diff_notes_bundle.js
app/assets/javascripts/diff_notes/diff_notes_bundle.js
+9
-6
app/assets/javascripts/diff_notes/services/resolve.js
app/assets/javascripts/diff_notes/services/resolve.js
+3
-2
app/assets/javascripts/lib/utils/common_utils.js
app/assets/javascripts/lib/utils/common_utils.js
+16
-5
app/assets/javascripts/lib/utils/text_utility.js
app/assets/javascripts/lib/utils/text_utility.js
+14
-0
app/assets/javascripts/merge_request_tabs.js
app/assets/javascripts/merge_request_tabs.js
+4
-0
app/assets/javascripts/mr_notes/index.js
app/assets/javascripts/mr_notes/index.js
+41
-0
app/assets/javascripts/notes.js
app/assets/javascripts/notes.js
+84
-54
app/assets/javascripts/notes/components/comment_form.vue
app/assets/javascripts/notes/components/comment_form.vue
+54
-23
app/assets/javascripts/notes/components/diff_file_header.vue
app/assets/javascripts/notes/components/diff_file_header.vue
+92
-0
app/assets/javascripts/notes/components/diff_with_note.vue
app/assets/javascripts/notes/components/diff_with_note.vue
+96
-0
app/assets/javascripts/notes/components/discussion_counter.vue
...ssets/javascripts/notes/components/discussion_counter.vue
+119
-0
app/assets/javascripts/notes/components/note_actions.vue
app/assets/javascripts/notes/components/note_actions.vue
+61
-0
app/assets/javascripts/notes/components/note_body.vue
app/assets/javascripts/notes/components/note_body.vue
+2
-2
app/assets/javascripts/notes/components/note_form.vue
app/assets/javascripts/notes/components/note_form.vue
+31
-12
app/assets/javascripts/notes/components/note_header.vue
app/assets/javascripts/notes/components/note_header.vue
+6
-7
app/assets/javascripts/notes/components/noteable_discussion.vue
...sets/javascripts/notes/components/noteable_discussion.vue
+156
-42
app/assets/javascripts/notes/components/noteable_note.vue
app/assets/javascripts/notes/components/noteable_note.vue
+15
-2
app/assets/javascripts/notes/components/notes_app.vue
app/assets/javascripts/notes/components/notes_app.vue
+34
-9
app/assets/javascripts/notes/constants.js
app/assets/javascripts/notes/constants.js
+5
-1
app/assets/javascripts/notes/index.js
app/assets/javascripts/notes/index.js
+1
-11
app/assets/javascripts/notes/mixins/autosave.js
app/assets/javascripts/notes/mixins/autosave.js
+3
-2
app/assets/javascripts/notes/mixins/noteable.js
app/assets/javascripts/notes/mixins/noteable.js
+22
-0
app/assets/javascripts/notes/mixins/resolvable.js
app/assets/javascripts/notes/mixins/resolvable.js
+50
-0
app/assets/javascripts/notes/services/notes_service.js
app/assets/javascripts/notes/services/notes_service.js
+7
-0
app/assets/javascripts/notes/stores/actions.js
app/assets/javascripts/notes/stores/actions.js
+13
-4
app/assets/javascripts/notes/stores/getters.js
app/assets/javascripts/notes/stores/getters.js
+35
-1
app/assets/javascripts/notes/stores/mutation_types.js
app/assets/javascripts/notes/stores/mutation_types.js
+1
-0
app/assets/javascripts/notes/stores/mutations.js
app/assets/javascripts/notes/stores/mutations.js
+40
-3
app/assets/javascripts/notes/stores/utils.js
app/assets/javascripts/notes/stores/utils.js
+0
-1
app/assets/javascripts/pages/projects/merge_requests/show/index.js
...s/javascripts/pages/projects/merge_requests/show/index.js
+0
-1
app/assets/javascripts/vue_shared/components/clipboard_button.vue
...ts/javascripts/vue_shared/components/clipboard_button.vue
+1
-1
app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
...javascripts/vue_shared/components/issue/issue_warning.vue
+0
-3
app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
...javascripts/vue_shared/components/notes/skeleton_note.vue
+24
-0
app/assets/stylesheets/pages/notes.scss
app/assets/stylesheets/pages/notes.scss
+1
-1
app/controllers/concerns/issuable_actions.rb
app/controllers/concerns/issuable_actions.rb
+14
-0
app/controllers/concerns/notes_actions.rb
app/controllers/concerns/notes_actions.rb
+12
-2
app/controllers/projects/discussions_controller.rb
app/controllers/projects/discussions_controller.rb
+32
-6
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+0
-14
app/controllers/projects/notes_controller.rb
app/controllers/projects/notes_controller.rb
+23
-7
app/helpers/notes_helper.rb
app/helpers/notes_helper.rb
+31
-0
app/models/note.rb
app/models/note.rb
+1
-0
app/serializers/diff_file_entity.rb
app/serializers/diff_file_entity.rb
+41
-0
app/serializers/discussion_entity.rb
app/serializers/discussion_entity.rb
+38
-0
app/serializers/merge_request_widget_entity.rb
app/serializers/merge_request_widget_entity.rb
+12
-0
app/serializers/note_entity.rb
app/serializers/note_entity.rb
+12
-0
app/views/projects/issues/_discussion.html.haml
app/views/projects/issues/_discussion.html.haml
+3
-11
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+1
-1
app/views/projects/merge_requests/show.html.haml
app/views/projects/merge_requests/show.html.haml
+30
-17
app/views/shared/notes/_notes_with_form.html.haml
app/views/shared/notes/_notes_with_form.html.haml
+4
-3
config/routes/project.rb
config/routes/project.rb
+1
-0
config/webpack.config.js
config/webpack.config.js
+2
-0
spec/controllers/projects/discussions_controller_spec.rb
spec/controllers/projects/discussions_controller_spec.rb
+26
-0
spec/controllers/projects/issues_controller_spec.rb
spec/controllers/projects/issues_controller_spec.rb
+1
-1
spec/features/merge_request/user_posts_notes_spec.rb
spec/features/merge_request/user_posts_notes_spec.rb
+1
-1
spec/fixtures/api/schemas/entities/merge_request_widget.json
spec/fixtures/api/schemas/entities/merge_request_widget.json
+4
-1
spec/javascripts/autosave_spec.js
spec/javascripts/autosave_spec.js
+39
-16
spec/javascripts/fixtures/merge_requests.rb
spec/javascripts/fixtures/merge_requests.rb
+42
-0
spec/javascripts/notes/components/comment_form_spec.js
spec/javascripts/notes/components/comment_form_spec.js
+20
-3
spec/javascripts/notes/components/diff_file_header_spec.js
spec/javascripts/notes/components/diff_file_header_spec.js
+93
-0
spec/javascripts/notes/components/diff_with_note_spec.js
spec/javascripts/notes/components/diff_with_note_spec.js
+64
-0
spec/javascripts/notes/components/note_app_spec.js
spec/javascripts/notes/components/note_app_spec.js
+3
-2
spec/javascripts/notes/components/note_body_spec.js
spec/javascripts/notes/components/note_body_spec.js
+18
-9
spec/javascripts/notes/components/note_header_spec.js
spec/javascripts/notes/components/note_header_spec.js
+26
-6
spec/javascripts/notes/mock_data.js
spec/javascripts/notes/mock_data.js
+3
-2
spec/javascripts/notes/stores/getters_spec.js
spec/javascripts/notes/stores/getters_spec.js
+2
-2
spec/javascripts/notes/stores/mutation_spec.js
spec/javascripts/notes/stores/mutation_spec.js
+3
-2
spec/javascripts/notes_spec.js
spec/javascripts/notes_spec.js
+6
-5
spec/serializers/diff_file_entity_spec.rb
spec/serializers/diff_file_entity_spec.rb
+24
-0
spec/serializers/discussion_entity_spec.rb
spec/serializers/discussion_entity_spec.rb
+36
-0
spec/serializers/note_entity_spec.rb
spec/serializers/note_entity_spec.rb
+11
-0
No files found.
app/assets/javascripts/autosave.js
View file @
059ab73b
...
@@ -3,10 +3,10 @@
...
@@ -3,10 +3,10 @@
import
AccessorUtilities
from
'
./lib/utils/accessor
'
;
import
AccessorUtilities
from
'
./lib/utils/accessor
'
;
export
default
class
Autosave
{
export
default
class
Autosave
{
constructor
(
field
,
key
,
resource
)
{
constructor
(
field
,
key
)
{
this
.
field
=
field
;
this
.
field
=
field
;
this
.
isLocalStorageAvailable
=
AccessorUtilities
.
isLocalStorageAccessSafe
();
this
.
isLocalStorageAvailable
=
AccessorUtilities
.
isLocalStorageAccessSafe
();
this
.
resource
=
resource
;
if
(
key
.
join
!=
null
)
{
if
(
key
.
join
!=
null
)
{
key
=
key
.
join
(
'
/
'
);
key
=
key
.
join
(
'
/
'
);
}
}
...
@@ -17,31 +17,27 @@ export default class Autosave {
...
@@ -17,31 +17,27 @@ export default class Autosave {
}
}
restore
()
{
restore
()
{
var
text
;
if
(
!
this
.
isLocalStorageAvailable
)
return
;
if
(
!
this
.
isLocalStorageAvailable
)
return
;
if
(
!
this
.
field
.
length
)
return
;
text
=
window
.
localStorage
.
getItem
(
this
.
key
);
const
text
=
window
.
localStorage
.
getItem
(
this
.
key
);
if
((
text
!=
null
?
text
.
length
:
void
0
)
>
0
)
{
if
((
text
!=
null
?
text
.
length
:
void
0
)
>
0
)
{
this
.
field
.
val
(
text
);
this
.
field
.
val
(
text
);
}
}
if
(
!
this
.
resource
&&
this
.
resource
!==
'
issue
'
)
{
this
.
field
.
trigger
(
'
input
'
);
this
.
field
.
trigger
(
'
input
'
);
}
else
{
// v-model does not update with jQuery trigger
// v-model does not update with jQuery trigger
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const
event
=
new
Event
(
'
change
'
,
{
bubbles
:
true
,
cancelable
:
false
});
const
event
=
new
Event
(
'
change
'
,
{
bubbles
:
true
,
cancelable
:
false
});
const
field
=
this
.
field
.
get
(
0
);
const
field
=
this
.
field
.
get
(
0
);
if
(
field
)
{
field
.
dispatchEvent
(
event
);
field
.
dispatchEvent
(
event
);
}
}
}
}
save
()
{
save
()
{
var
text
;
if
(
!
this
.
field
.
length
)
return
;
text
=
this
.
field
.
val
();
const
text
=
this
.
field
.
val
();
if
(
this
.
isLocalStorageAvailable
&&
(
text
!=
null
?
text
.
length
:
void
0
)
>
0
)
{
if
(
this
.
isLocalStorageAvailable
&&
(
text
!=
null
?
text
.
length
:
void
0
)
>
0
)
{
return
window
.
localStorage
.
setItem
(
this
.
key
,
text
);
return
window
.
localStorage
.
setItem
(
this
.
key
,
text
);
...
...
app/assets/javascripts/awards_handler.js
View file @
059ab73b
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
import
_
from
'
underscore
'
;
import
_
from
'
underscore
'
;
import
Cookies
from
'
js-cookie
'
;
import
Cookies
from
'
js-cookie
'
;
import
{
__
}
from
'
./locale
'
;
import
{
__
}
from
'
./locale
'
;
import
{
isInIssuePage
,
updateTooltipTitle
}
from
'
./lib/utils/common_utils
'
;
import
{
isInIssuePage
,
isInMRPage
,
hasVueMRDiscussionsCookie
,
updateTooltipTitle
}
from
'
./lib/utils/common_utils
'
;
import
flash
from
'
./flash
'
;
import
flash
from
'
./flash
'
;
import
axios
from
'
./lib/utils/axios_utils
'
;
import
axios
from
'
./lib/utils/axios_utils
'
;
...
@@ -239,9 +239,9 @@ class AwardsHandler {
...
@@ -239,9 +239,9 @@ class AwardsHandler {
}
}
addAward
(
votesBlock
,
awardUrl
,
emoji
,
checkMutuality
,
callback
)
{
addAward
(
votesBlock
,
awardUrl
,
emoji
,
checkMutuality
,
callback
)
{
const
isMainAwardsBlock
=
votesBlock
.
closest
(
'
.js-
issue-not
e-awards
'
).
length
;
const
isMainAwardsBlock
=
votesBlock
.
closest
(
'
.js-
noteabl
e-awards
'
).
length
;
if
(
isInIssu
ePage
()
&&
!
isMainAwardsBlock
)
{
if
(
this
.
isInVueNoteabl
ePage
()
&&
!
isMainAwardsBlock
)
{
const
id
=
votesBlock
.
attr
(
'
id
'
).
replace
(
'
note_
'
,
''
);
const
id
=
votesBlock
.
attr
(
'
id
'
).
replace
(
'
note_
'
,
''
);
this
.
hideMenuElement
(
$
(
'
.emoji-menu
'
));
this
.
hideMenuElement
(
$
(
'
.emoji-menu
'
));
...
@@ -293,8 +293,16 @@ class AwardsHandler {
...
@@ -293,8 +293,16 @@ class AwardsHandler {
}
}
}
}
isVueMRDiscussions
()
{
return
isInMRPage
()
&&
hasVueMRDiscussionsCookie
()
&&
!
$
(
'
#diffs
'
).
is
(
'
:visible
'
);
}
isInVueNoteablePage
()
{
return
isInIssuePage
()
||
this
.
isVueMRDiscussions
();
}
getVotesBlock
()
{
getVotesBlock
()
{
if
(
isInIssu
ePage
())
{
if
(
this
.
isInVueNoteabl
ePage
())
{
const
$el
=
$
(
'
.js-add-award.is-active
'
).
closest
(
'
.note.timeline-entry
'
);
const
$el
=
$
(
'
.js-add-award.is-active
'
).
closest
(
'
.note.timeline-entry
'
);
if
(
$el
.
length
)
{
if
(
$el
.
length
)
{
...
...
app/assets/javascripts/diff_notes/components/jump_to_discussion.js
View file @
059ab73b
...
@@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({
...
@@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({
}
}
$
.
scrollTo
(
$target
,
{
$
.
scrollTo
(
$target
,
{
offset
:
0
offset
:
-
15
0
});
});
}
}
},
},
...
...
app/assets/javascripts/diff_notes/components/resolve_btn.js
View file @
059ab73b
...
@@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({
...
@@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({
CommentsStore
.
update
(
this
.
discussionId
,
this
.
noteId
,
!
this
.
isResolved
,
resolved_by
);
CommentsStore
.
update
(
this
.
discussionId
,
this
.
noteId
,
!
this
.
isResolved
,
resolved_by
);
this
.
discussion
.
updateHeadline
(
data
);
this
.
discussion
.
updateHeadline
(
data
);
gl
.
mrWidget
.
checkStatus
();
gl
.
mrWidget
.
checkStatus
();
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshVueNotes
'
));
this
.
updateTooltip
();
this
.
updateTooltip
();
})
})
...
...
app/assets/javascripts/diff_notes/diff_notes_bundle.js
View file @
059ab73b
...
@@ -14,6 +14,7 @@ import './components/resolve_count';
...
@@ -14,6 +14,7 @@ import './components/resolve_count';
import
'
./components/resolve_discussion_btn
'
;
import
'
./components/resolve_discussion_btn
'
;
import
'
./components/diff_note_avatars
'
;
import
'
./components/diff_note_avatars
'
;
import
'
./components/new_issue_for_discussion
'
;
import
'
./components/new_issue_for_discussion
'
;
import
{
hasVueMRDiscussionsCookie
}
from
'
../lib/utils/common_utils
'
;
export
default
()
=>
{
export
default
()
=>
{
const
projectPathHolder
=
document
.
querySelector
(
'
.merge-request
'
)
||
document
.
querySelector
(
'
.commit-box
'
);
const
projectPathHolder
=
document
.
querySelector
(
'
.merge-request
'
)
||
document
.
querySelector
(
'
.commit-box
'
);
...
@@ -67,12 +68,14 @@ export default () => {
...
@@ -67,12 +68,14 @@ export default () => {
gl
.
diffNotesCompileComponents
();
gl
.
diffNotesCompileComponents
();
if
(
!
hasVueMRDiscussionsCookie
())
{
new
Vue
({
new
Vue
({
el
:
'
#resolve-count-app
'
,
el
:
'
#resolve-count-app
'
,
components
:
{
components
:
{
'
resolve-count
'
:
ResolveCount
'
resolve-count
'
:
ResolveCount
},
},
});
});
}
$
(
window
).
trigger
(
'
resize.nav
'
);
$
(
window
).
trigger
(
'
resize.nav
'
);
};
};
app/assets/javascripts/diff_notes/services/resolve.js
View file @
059ab73b
...
@@ -8,8 +8,8 @@ window.gl = window.gl || {};
...
@@ -8,8 +8,8 @@ window.gl = window.gl || {};
class
ResolveServiceClass
{
class
ResolveServiceClass
{
constructor
(
root
)
{
constructor
(
root
)
{
this
.
noteResource
=
Vue
.
resource
(
`
${
root
}
/notes{/noteId}/resolve`
);
this
.
noteResource
=
Vue
.
resource
(
`
${
root
}
/notes{/noteId}/resolve
?html=true
`
);
this
.
discussionResource
=
Vue
.
resource
(
`
${
root
}
/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`
);
this
.
discussionResource
=
Vue
.
resource
(
`
${
root
}
/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve
?html=true
`
);
}
}
resolve
(
noteId
)
{
resolve
(
noteId
)
{
...
@@ -45,6 +45,7 @@ class ResolveServiceClass {
...
@@ -45,6 +45,7 @@ class ResolveServiceClass {
if
(
gl
.
mrWidget
)
gl
.
mrWidget
.
checkStatus
();
if
(
gl
.
mrWidget
)
gl
.
mrWidget
.
checkStatus
();
discussion
.
updateHeadline
(
data
);
discussion
.
updateHeadline
(
data
);
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshVueNotes
'
));
})
})
.
catch
(()
=>
new
Flash
(
'
An error occurred when trying to resolve a discussion. Please try again.
'
));
.
catch
(()
=>
new
Flash
(
'
An error occurred when trying to resolve a discussion. Please try again.
'
));
}
}
...
...
app/assets/javascripts/lib/utils/common_utils.js
View file @
059ab73b
import
jQuery
from
'
jquery
'
;
import
Cookies
from
'
js-cookie
'
;
import
axios
from
'
./axios_utils
'
;
import
axios
from
'
./axios_utils
'
;
import
{
getLocationHash
}
from
'
./url_utility
'
;
import
{
getLocationHash
}
from
'
./url_utility
'
;
import
{
convertToCamelCase
}
from
'
./text_utility
'
;
import
{
convertToCamelCase
}
from
'
./text_utility
'
;
...
@@ -22,13 +24,18 @@ export const getGroupSlug = () => {
...
@@ -22,13 +24,18 @@ export const getGroupSlug = () => {
return
null
;
return
null
;
};
};
export
const
isInIssuePage
=
(
)
=>
{
export
const
checkPageAndAction
=
(
page
,
action
)
=>
{
const
page
=
getPagePath
(
1
);
const
page
Path
=
getPagePath
(
1
);
const
action
=
getPagePath
(
2
);
const
action
Path
=
getPagePath
(
2
);
return
page
===
'
issues
'
&&
action
===
'
show
'
;
return
page
Path
===
page
&&
actionPath
===
action
;
};
};
export
const
isInIssuePage
=
()
=>
checkPageAndAction
(
'
issues
'
,
'
show
'
);
export
const
isInMRPage
=
()
=>
checkPageAndAction
(
'
merge_requests
'
,
'
show
'
);
export
const
isInNoteablePage
=
()
=>
isInIssuePage
()
||
isInMRPage
();
export
const
hasVueMRDiscussionsCookie
=
()
=>
Cookies
.
get
(
'
vue_mr_discussions
'
);
export
const
ajaxGet
=
url
=>
axios
.
get
(
url
,
{
export
const
ajaxGet
=
url
=>
axios
.
get
(
url
,
{
params
:
{
format
:
'
js
'
},
params
:
{
format
:
'
js
'
},
responseType
:
'
text
'
,
responseType
:
'
text
'
,
...
@@ -133,7 +140,11 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
...
@@ -133,7 +140,11 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export
const
isMetaClick
=
e
=>
e
.
metaKey
||
e
.
ctrlKey
||
e
.
which
===
2
;
export
const
isMetaClick
=
e
=>
e
.
metaKey
||
e
.
ctrlKey
||
e
.
which
===
2
;
export
const
scrollToElement
=
(
$el
)
=>
{
export
const
scrollToElement
=
(
element
)
=>
{
let
$el
=
element
;
if
(
!
(
element
instanceof
jQuery
))
{
$el
=
$
(
element
);
}
const
top
=
$el
.
offset
().
top
;
const
top
=
$el
.
offset
().
top
;
const
mrTabsHeight
=
$
(
'
.merge-request-tabs
'
).
height
()
||
0
;
const
mrTabsHeight
=
$
(
'
.merge-request-tabs
'
).
height
()
||
0
;
const
headerHeight
=
$
(
'
.navbar-gitlab
'
).
height
()
||
0
;
const
headerHeight
=
$
(
'
.navbar-gitlab
'
).
height
()
||
0
;
...
...
app/assets/javascripts/lib/utils/text_utility.js
View file @
059ab73b
...
@@ -65,6 +65,20 @@ export function capitalizeFirstCharacter(text) {
...
@@ -65,6 +65,20 @@ export function capitalizeFirstCharacter(text) {
return
`
${
text
[
0
].
toUpperCase
()}${
text
.
slice
(
1
)}
`
;
return
`
${
text
[
0
].
toUpperCase
()}${
text
.
slice
(
1
)}
`
;
}
}
export
function
camelCase
(
str
)
{
return
str
.
replace
(
/_+
([
a-z
])
/gi
,
(
$1
,
$2
)
=>
$2
.
toUpperCase
());
}
export
function
camelCaseKeys
(
obj
=
{})
{
return
Object
.
keys
(
obj
).
reduce
((
acc
,
key
)
=>
{
const
camelKey
=
camelCase
(
key
);
return
{
...
acc
,
[
camelKey
]:
obj
[
key
],
};
},
{});
}
/**
/**
* Replaces all html tags from a string with the given replacement.
* Replaces all html tags from a string with the given replacement.
*
*
...
...
app/assets/javascripts/merge_request_tabs.js
View file @
059ab73b
...
@@ -241,6 +241,10 @@ export default class MergeRequestTabs {
...
@@ -241,6 +241,10 @@ export default class MergeRequestTabs {
return
newState
;
return
newState
;
}
}
getCurrentAction
()
{
return
this
.
currentAction
;
}
loadCommits
(
source
)
{
loadCommits
(
source
)
{
if
(
this
.
commitsLoaded
)
{
if
(
this
.
commitsLoaded
)
{
return
;
return
;
...
...
app/assets/javascripts/mr_notes/index.js
0 → 100644
View file @
059ab73b
import
Vue
from
'
vue
'
;
import
notesApp
from
'
../notes/components/notes_app.vue
'
;
import
discussionCounter
from
'
../notes/components/discussion_counter.vue
'
;
import
store
from
'
../notes/stores
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
new
Vue
({
// eslint-disable-line
el
:
'
#js-vue-mr-discussions
'
,
components
:
{
notesApp
,
},
data
()
{
const
notesDataset
=
document
.
getElementById
(
'
js-vue-mr-discussions
'
).
dataset
;
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
:
JSON
.
parse
(
notesDataset
.
currentUserData
),
notesData
:
JSON
.
parse
(
notesDataset
.
notesData
),
};
},
render
(
createElement
)
{
return
createElement
(
'
notes-app
'
,
{
props
:
{
noteableData
:
this
.
noteableData
,
notesData
:
this
.
notesData
,
userData
:
this
.
currentUserData
,
},
});
},
});
new
Vue
({
// eslint-disable-line
el
:
'
#js-vue-discussion-counter
'
,
components
:
{
discussionCounter
,
},
store
,
render
(
createElement
)
{
return
createElement
(
'
discussion-counter
'
);
},
});
});
app/assets/javascripts/notes.js
View file @
059ab73b
...
@@ -24,7 +24,7 @@ import GLForm from './gl_form';
...
@@ -24,7 +24,7 @@ import GLForm from './gl_form';
import
loadAwardsHandler
from
'
./awards_handler
'
;
import
loadAwardsHandler
from
'
./awards_handler
'
;
import
Autosave
from
'
./autosave
'
;
import
Autosave
from
'
./autosave
'
;
import
TaskList
from
'
./task_list
'
;
import
TaskList
from
'
./task_list
'
;
import
{
isInViewport
,
getPagePath
,
scrollToElement
,
isMetaKey
}
from
'
./lib/utils/common_utils
'
;
import
{
isInViewport
,
getPagePath
,
scrollToElement
,
isMetaKey
,
hasVueMRDiscussionsCookie
}
from
'
./lib/utils/common_utils
'
;
import
imageDiffHelper
from
'
./image_diff/helpers/index
'
;
import
imageDiffHelper
from
'
./image_diff/helpers/index
'
;
import
{
localTimeAgo
}
from
'
./lib/utils/datetime_utility
'
;
import
{
localTimeAgo
}
from
'
./lib/utils/datetime_utility
'
;
...
@@ -44,6 +44,10 @@ export default class Notes {
...
@@ -44,6 +44,10 @@ export default class Notes {
}
}
}
}
static
getInstance
()
{
return
this
.
instance
;
}
constructor
(
notes_url
,
note_ids
,
last_fetched_at
,
view
,
enableGFM
=
true
)
{
constructor
(
notes_url
,
note_ids
,
last_fetched_at
,
view
,
enableGFM
=
true
)
{
this
.
updateTargetButtons
=
this
.
updateTargetButtons
.
bind
(
this
);
this
.
updateTargetButtons
=
this
.
updateTargetButtons
.
bind
(
this
);
this
.
updateComment
=
this
.
updateComment
.
bind
(
this
);
this
.
updateComment
=
this
.
updateComment
.
bind
(
this
);
...
@@ -102,67 +106,77 @@ export default class Notes {
...
@@ -102,67 +106,77 @@ export default class Notes {
}
}
addBinding
()
{
addBinding
()
{
this
.
$wrapperEl
=
hasVueMRDiscussionsCookie
()
?
$
(
document
).
find
(
'
.diffs
'
)
:
$
(
document
);
// Edit note link
// Edit note link
$
(
document
)
.
on
(
'
click
'
,
'
.js-note-edit
'
,
this
.
showEditForm
.
bind
(
this
));
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-edit
'
,
this
.
showEditForm
.
bind
(
this
));
$
(
document
)
.
on
(
'
click
'
,
'
.note-edit-cancel
'
,
this
.
cancelEdit
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.note-edit-cancel
'
,
this
.
cancelEdit
);
// Reopen and close actions for Issue/MR combined with note form submit
// Reopen and close actions for Issue/MR combined with note form submit
$
(
document
)
.
on
(
'
click
'
,
'
.js-comment-submit-button
'
,
this
.
postComment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-submit-button
'
,
this
.
postComment
);
$
(
document
)
.
on
(
'
click
'
,
'
.js-comment-save-button
'
,
this
.
updateComment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-save-button
'
,
this
.
updateComment
);
$
(
document
)
.
on
(
'
keyup input
'
,
'
.js-note-text
'
,
this
.
updateTargetButtons
);
this
.
$wrapperEl
.
on
(
'
keyup input
'
,
'
.js-note-text
'
,
this
.
updateTargetButtons
);
// resolve a discussion
// resolve a discussion
$
(
document
)
.
on
(
'
click
'
,
'
.js-comment-resolve-button
'
,
this
.
postComment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-resolve-button
'
,
this
.
postComment
);
// remove a note (in general)
// remove a note (in general)
$
(
document
)
.
on
(
'
click
'
,
'
.js-note-delete
'
,
this
.
removeNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-delete
'
,
this
.
removeNote
);
// delete note attachment
// delete note attachment
$
(
document
)
.
on
(
'
click
'
,
'
.js-note-attachment-delete
'
,
this
.
removeAttachment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-attachment-delete
'
,
this
.
removeAttachment
);
// reset main target form when clicking discard
// reset main target form when clicking discard
$
(
document
)
.
on
(
'
click
'
,
'
.js-note-discard
'
,
this
.
resetMainTargetForm
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-discard
'
,
this
.
resetMainTargetForm
);
// update the file name when an attachment is selected
// update the file name when an attachment is selected
$
(
document
)
.
on
(
'
change
'
,
'
.js-note-attachment-input
'
,
this
.
updateFormAttachment
);
this
.
$wrapperEl
.
on
(
'
change
'
,
'
.js-note-attachment-input
'
,
this
.
updateFormAttachment
);
// reply to diff/discussion notes
// reply to diff/discussion notes
$
(
document
)
.
on
(
'
click
'
,
'
.js-discussion-reply-button
'
,
this
.
onReplyToDiscussionNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-discussion-reply-button
'
,
this
.
onReplyToDiscussionNote
);
// add diff note
// add diff note
$
(
document
)
.
on
(
'
click
'
,
'
.js-add-diff-note-button
'
,
this
.
onAddDiffNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-add-diff-note-button
'
,
this
.
onAddDiffNote
);
// add diff note for images
// add diff note for images
$
(
document
)
.
on
(
'
click
'
,
'
.js-add-image-diff-note-button
'
,
this
.
onAddImageDiffNote
);
this
.
$wrapperEl
.
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
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-close-discussion-note-form
'
,
this
.
cancelDiscussionForm
);
// toggle commit list
// toggle commit list
$
(
document
)
.
on
(
'
click
'
,
'
.system-note-commit-list-toggler
'
,
this
.
toggleCommitList
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.system-note-commit-list-toggler
'
,
this
.
toggleCommitList
);
// fetch notes when tab becomes visible
// fetch notes when tab becomes visible
$
(
document
)
.
on
(
'
visibilitychange
'
,
this
.
visibilityChange
);
this
.
$wrapperEl
.
on
(
'
visibilitychange
'
,
this
.
visibilityChange
);
// when issue status changes, we need to refresh data
// when issue status changes, we need to refresh data
$
(
document
)
.
on
(
'
issuable:change
'
,
this
.
refresh
);
this
.
$wrapperEl
.
on
(
'
issuable:change
'
,
this
.
refresh
);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
$
(
document
)
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
addNote
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
addNote
);
$
(
document
)
.
on
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
,
this
.
addDiscussionNote
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
,
this
.
addDiscussionNote
);
$
(
document
)
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
resetMainTargetForm
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
resetMainTargetForm
);
$
(
document
)
.
on
(
'
ajax:complete
'
,
'
.js-main-target-form
'
,
this
.
reenableTargetFormSubmitButton
);
this
.
$wrapperEl
.
on
(
'
ajax:complete
'
,
'
.js-main-target-form
'
,
this
.
reenableTargetFormSubmitButton
);
// when a key is clicked on the notes
// when a key is clicked on the notes
$
(
document
)
.
on
(
'
keydown
'
,
'
.js-note-text
'
,
this
.
keydownNoteText
);
this
.
$wrapperEl
.
on
(
'
keydown
'
,
'
.js-note-text
'
,
this
.
keydownNoteText
);
// When the URL fragment/hash has changed, `#note_xxx`
// When the URL fragment/hash has changed, `#note_xxx`
return
$
(
window
).
on
(
'
hashchange
'
,
this
.
onHashChange
);
$
(
window
).
on
(
'
hashchange
'
,
this
.
onHashChange
);
this
.
boundGetContent
=
this
.
getContent
.
bind
(
this
);
document
.
addEventListener
(
'
refreshLegacyNotes
'
,
this
.
boundGetContent
);
this
.
eventsBound
=
true
;
}
}
cleanBinding
()
{
cleanBinding
()
{
$
(
document
).
off
(
'
click
'
,
'
.js-note-edit
'
);
if
(
!
this
.
eventsBound
)
{
$
(
document
).
off
(
'
click
'
,
'
.note-edit-cancel
'
);
return
;
$
(
document
).
off
(
'
click
'
,
'
.js-note-delete
'
);
}
$
(
document
).
off
(
'
click
'
,
'
.js-note-attachment-delete
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-discussion-reply-button
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-edit
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-add-diff-note-button
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.note-edit-cancel
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-add-image-diff-note-button
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-delete
'
);
$
(
document
).
off
(
'
visibilitychange
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-attachment-delete
'
);
$
(
document
).
off
(
'
keyup input
'
,
'
.js-note-text
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-discussion-reply-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-target-reopen
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-add-diff-note-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-target-close
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-add-image-diff-note-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-discard
'
);
this
.
$wrapperEl
.
off
(
'
visibilitychange
'
);
$
(
document
).
off
(
'
keydown
'
,
'
.js-note-text
'
);
this
.
$wrapperEl
.
off
(
'
keyup input
'
,
'
.js-note-text
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-comment-resolve-button
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-target-reopen
'
);
$
(
document
).
off
(
'
click
'
,
'
.system-note-commit-list-toggler
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-target-close
'
);
$
(
document
).
off
(
'
ajax:success
'
,
'
.js-main-target-form
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-discard
'
);
$
(
document
).
off
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
);
this
.
$wrapperEl
.
off
(
'
keydown
'
,
'
.js-note-text
'
);
$
(
document
).
off
(
'
ajax:complete
'
,
'
.js-main-target-form
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-comment-resolve-button
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.system-note-commit-list-toggler
'
);
this
.
$wrapperEl
.
off
(
'
ajax:success
'
,
'
.js-main-target-form
'
);
this
.
$wrapperEl
.
off
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
);
this
.
$wrapperEl
.
off
(
'
ajax:complete
'
,
'
.js-main-target-form
'
);
document
.
removeEventListener
(
'
refreshLegacyNotes
'
,
this
.
boundGetContent
);
$
(
window
).
off
(
'
hashchange
'
,
this
.
onHashChange
);
$
(
window
).
off
(
'
hashchange
'
,
this
.
onHashChange
);
}
}
...
@@ -252,8 +266,10 @@ export default class Notes {
...
@@ -252,8 +266,10 @@ export default class Notes {
if
(
this
.
refreshing
)
{
if
(
this
.
refreshing
)
{
return
;
return
;
}
}
this
.
refreshing
=
true
;
this
.
refreshing
=
true
;
axios
.
get
(
this
.
notes_url
,
{
axios
.
get
(
`
${
this
.
notes_url
}
?html=true`
,
{
headers
:
{
headers
:
{
'
X-Last-Fetched-At
'
:
this
.
last_fetched_at
,
'
X-Last-Fetched-At
'
:
this
.
last_fetched_at
,
},
},
...
@@ -350,7 +366,7 @@ export default class Notes {
...
@@ -350,7 +366,7 @@ export default class Notes {
}
}
if
(
!
noteEntity
.
valid
)
{
if
(
!
noteEntity
.
valid
)
{
if
(
noteEntity
.
errors
.
commands_only
)
{
if
(
noteEntity
.
errors
&&
noteEntity
.
errors
.
commands_only
)
{
if
(
noteEntity
.
commands_changes
&&
if
(
noteEntity
.
commands_changes
&&
Object
.
keys
(
noteEntity
.
commands_changes
).
length
>
0
)
{
Object
.
keys
(
noteEntity
.
commands_changes
).
length
>
0
)
{
$notesList
.
find
(
'
.system-note.being-posted
'
).
remove
();
$notesList
.
find
(
'
.system-note.being-posted
'
).
remove
();
...
@@ -363,6 +379,10 @@ export default class Notes {
...
@@ -363,6 +379,10 @@ export default class Notes {
const
$note
=
$notesList
.
find
(
`#note_
${
noteEntity
.
id
}
`
);
const
$note
=
$notesList
.
find
(
`#note_
${
noteEntity
.
id
}
`
);
if
(
Notes
.
isNewNote
(
noteEntity
,
this
.
note_ids
))
{
if
(
Notes
.
isNewNote
(
noteEntity
,
this
.
note_ids
))
{
if
(
hasVueMRDiscussionsCookie
())
{
return
;
}
this
.
note_ids
.
push
(
noteEntity
.
id
);
this
.
note_ids
.
push
(
noteEntity
.
id
);
if
(
$notesList
.
length
)
{
if
(
$notesList
.
length
)
{
...
@@ -399,6 +419,8 @@ export default class Notes {
...
@@ -399,6 +419,8 @@ export default class Notes {
this
.
setupNewNote
(
$updatedNote
);
this
.
setupNewNote
(
$updatedNote
);
}
}
}
}
Notes
.
refreshVueNotes
();
}
}
isParallelView
()
{
isParallelView
()
{
...
@@ -406,12 +428,11 @@ export default class Notes {
...
@@ -406,12 +428,11 @@ export default class Notes {
}
}
/**
/**
* Render note in discussion area.
* Render note in discussion area. To render inline notes use renderDiscussionNote.
*
* Note: for rendering inline notes use renderDiscussionNote
*/
*/
renderDiscussionNote
(
noteEntity
,
$form
)
{
renderDiscussionNote
(
noteEntity
,
$form
)
{
var
discussionContainer
,
form
,
row
,
lineType
,
diffAvatarContainer
;
var
discussionContainer
,
form
,
row
,
lineType
,
diffAvatarContainer
;
if
(
!
Notes
.
isNewNote
(
noteEntity
,
this
.
note_ids
))
{
if
(
!
Notes
.
isNewNote
(
noteEntity
,
this
.
note_ids
))
{
return
;
return
;
}
}
...
@@ -452,8 +473,10 @@ export default class Notes {
...
@@ -452,8 +473,10 @@ export default class Notes {
// Init discussion on 'Discussion' page if it is merge request page
// Init discussion on 'Discussion' page if it is merge request page
const
page
=
$
(
'
body
'
).
attr
(
'
data-page
'
);
const
page
=
$
(
'
body
'
).
attr
(
'
data-page
'
);
if
((
page
&&
page
.
indexOf
(
'
projects:merge_request
'
)
!==
-
1
)
||
!
noteEntity
.
diff_discussion_html
)
{
if
((
page
&&
page
.
indexOf
(
'
projects:merge_request
'
)
!==
-
1
)
||
!
noteEntity
.
diff_discussion_html
)
{
if
(
!
hasVueMRDiscussionsCookie
())
{
Notes
.
animateAppendNote
(
noteEntity
.
discussion_html
,
$
(
'
.main-notes-list
'
));
Notes
.
animateAppendNote
(
noteEntity
.
discussion_html
,
$
(
'
.main-notes-list
'
));
}
}
}
}
else
{
}
else
{
// append new note to all matching discussions
// append new note to all matching discussions
Notes
.
animateAppendNote
(
noteEntity
.
html
,
discussionContainer
);
Notes
.
animateAppendNote
(
noteEntity
.
html
,
discussionContainer
);
...
@@ -634,7 +657,6 @@ export default class Notes {
...
@@ -634,7 +657,6 @@ export default class Notes {
var
$noteEntityEl
,
$note_li
;
var
$noteEntityEl
,
$note_li
;
// Convert returned HTML to a jQuery object so we can modify it further
// Convert returned HTML to a jQuery object so we can modify it further
$noteEntityEl
=
$
(
noteEntity
.
html
);
$noteEntityEl
=
$
(
noteEntity
.
html
);
$noteEntityEl
.
addClass
(
'
fade-in-full
'
);
this
.
revertNoteEditForm
(
$targetNote
);
this
.
revertNoteEditForm
(
$targetNote
);
$noteEntityEl
.
renderGFM
();
$noteEntityEl
.
renderGFM
();
// Find the note's `li` element by ID and replace it with the updated HTML
// Find the note's `li` element by ID and replace it with the updated HTML
...
@@ -730,7 +752,7 @@ export default class Notes {
...
@@ -730,7 +752,7 @@ export default class Notes {
var
selector
=
this
.
getEditFormSelector
(
$target
);
var
selector
=
this
.
getEditFormSelector
(
$target
);
var
$editForm
=
$
(
selector
);
var
$editForm
=
$
(
selector
);
$editForm
.
insertBefore
(
'
.
notes-form
'
);
$editForm
.
insertBefore
(
'
.
diffs
'
);
$editForm
.
find
(
'
.js-comment-save-button
'
).
enable
();
$editForm
.
find
(
'
.js-comment-save-button
'
).
enable
();
$editForm
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
$editForm
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
}
}
...
@@ -746,7 +768,8 @@ export default class Notes {
...
@@ -746,7 +768,8 @@ export default class Notes {
}
}
removeNoteEditForm
(
$note
)
{
removeNoteEditForm
(
$note
)
{
var
form
=
$note
.
find
(
'
.current-note-edit-form
'
);
var
form
=
$note
.
find
(
'
.diffs .current-note-edit-form
'
);
$note
.
removeClass
(
'
is-editing
'
);
$note
.
removeClass
(
'
is-editing
'
);
form
.
removeClass
(
'
current-note-edit-form
'
);
form
.
removeClass
(
'
current-note-edit-form
'
);
form
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
form
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
...
@@ -818,6 +841,7 @@ export default class Notes {
...
@@ -818,6 +841,7 @@ export default class Notes {
};
};
})(
this
));
})(
this
));
Notes
.
refreshVueNotes
();
Notes
.
checkMergeRequestStatus
();
Notes
.
checkMergeRequestStatus
();
return
this
.
updateNotesCount
(
-
1
);
return
this
.
updateNotesCount
(
-
1
);
}
}
...
@@ -1157,7 +1181,7 @@ export default class Notes {
...
@@ -1157,7 +1181,7 @@ export default class Notes {
this
.
glForm
=
new
GLForm
(
$editForm
.
find
(
'
form
'
),
this
.
enableGFM
);
this
.
glForm
=
new
GLForm
(
$editForm
.
find
(
'
form
'
),
this
.
enableGFM
);
$editForm
.
find
(
'
form
'
)
$editForm
.
find
(
'
form
'
)
.
attr
(
'
action
'
,
postUrl
)
.
attr
(
'
action
'
,
`
${
postUrl
}
?html=true`
)
.
attr
(
'
data-remote
'
,
'
true
'
);
.
attr
(
'
data-remote
'
,
'
true
'
);
$editForm
.
find
(
'
.js-form-target-id
'
).
val
(
targetId
);
$editForm
.
find
(
'
.js-form-target-id
'
).
val
(
targetId
);
$editForm
.
find
(
'
.js-form-target-type
'
).
val
(
targetType
);
$editForm
.
find
(
'
.js-form-target-type
'
).
val
(
targetType
);
...
@@ -1280,6 +1304,10 @@ export default class Notes {
...
@@ -1280,6 +1304,10 @@ export default class Notes {
return
$updatedNote
;
return
$updatedNote
;
}
}
static
refreshVueNotes
()
{
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshVueNotes
'
));
}
/**
/**
* Get data from Form attributes to use for saving/submitting comment.
* Get data from Form attributes to use for saving/submitting comment.
*/
*/
...
@@ -1481,7 +1509,7 @@ export default class Notes {
...
@@ -1481,7 +1509,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
// Make request to submit comment on server
axios
.
post
(
formAction
,
formData
)
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
((
res
)
=>
{
.
then
((
res
)
=>
{
const
note
=
res
.
data
;
const
note
=
res
.
data
;
...
@@ -1546,6 +1574,8 @@ export default class Notes {
...
@@ -1546,6 +1574,8 @@ export default class Notes {
if
(
$notesContainer
.
length
)
{
if
(
$notesContainer
.
length
)
{
$notesContainer
.
append
(
'
<div class="flash-container" style="display: none;"></div>
'
);
$notesContainer
.
append
(
'
<div class="flash-container" style="display: none;"></div>
'
);
}
}
Notes
.
refreshVueNotes
();
}
else
if
(
isMainForm
)
{
// Check if this was main thread comment
}
else
if
(
isMainForm
)
{
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
// Show final note element on UI and perform form and action buttons cleanup
this
.
addNote
(
$form
,
note
);
this
.
addNote
(
$form
,
note
);
...
@@ -1627,7 +1657,7 @@ export default class Notes {
...
@@ -1627,7 +1657,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
// Make request to update comment on server
axios
.
post
(
formAction
,
formData
)
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
(({
data
})
=>
{
.
then
(({
data
})
=>
{
// Submission successful! render final note element
// Submission successful! render final note element
this
.
updateNote
(
data
,
$editingNote
);
this
.
updateNote
(
data
,
$editingNote
);
...
...
app/assets/javascripts/notes/components/comment_form.vue
View file @
059ab73b
...
@@ -2,10 +2,11 @@
...
@@ -2,10 +2,11 @@
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
_
from
'
underscore
'
;
import
_
from
'
underscore
'
;
import
Autosize
from
'
autosize
'
;
import
Autosize
from
'
autosize
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
Flash
from
'
../../flash
'
;
import
Flash
from
'
../../flash
'
;
import
Autosave
from
'
../../autosave
'
;
import
Autosave
from
'
../../autosave
'
;
import
TaskList
from
'
../../task_list
'
;
import
TaskList
from
'
../../task_list
'
;
import
{
capitalizeFirstCharacter
,
convertToCamelCase
}
from
'
../../lib/utils/text_utility
'
;
import
*
as
constants
from
'
../constants
'
;
import
*
as
constants
from
'
../constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
...
@@ -29,6 +30,12 @@
...
@@ -29,6 +30,12 @@
mixins
:
[
mixins
:
[
issuableStateMixin
,
issuableStateMixin
,
],
],
props
:
{
noteableType
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
data
()
{
return
{
return
{
note
:
''
,
note
:
''
,
...
@@ -43,37 +50,51 @@
...
@@ -43,37 +50,51 @@
'
getUserData
'
,
'
getUserData
'
,
'
getNoteableData
'
,
'
getNoteableData
'
,
'
getNotesData
'
,
'
getNotesData
'
,
'
issue
State
'
,
'
open
State
'
,
]),
]),
noteableDisplayName
()
{
return
this
.
noteableType
.
replace
(
/_/g
,
'
'
);
},
isLoggedIn
()
{
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
return
this
.
getUserData
.
id
;
},
},
commentButtonTitle
()
{
commentButtonTitle
()
{
return
this
.
noteType
===
constants
.
COMMENT
?
'
Comment
'
:
'
Start discussion
'
;
return
this
.
noteType
===
constants
.
COMMENT
?
'
Comment
'
:
'
Start discussion
'
;
},
},
is
Issue
Open
()
{
isOpen
()
{
return
this
.
issueState
===
constants
.
OPENED
||
this
.
issue
State
===
constants
.
REOPENED
;
return
this
.
openState
===
constants
.
OPENED
||
this
.
open
State
===
constants
.
REOPENED
;
},
},
canCreateNote
()
{
canCreateNote
()
{
return
this
.
getNoteableData
.
current_user
.
can_create_note
;
return
this
.
getNoteableData
.
current_user
.
can_create_note
;
},
},
issueActionButtonTitle
()
{
issueActionButtonTitle
()
{
if
(
this
.
note
.
length
)
{
const
openOrClose
=
this
.
isOpen
?
'
close
'
:
'
reopen
'
;
const
actionText
=
this
.
isIssueOpen
?
'
close
'
:
'
reopen
'
;
return
this
.
noteType
===
constants
.
COMMENT
?
if
(
this
.
note
.
length
)
{
`Comment &
${
actionText
}
issue`
:
return
sprintf
(
`Start discussion &
${
actionText
}
issue`
;
__
(
'
%{actionText} & %{openOrClose} %{noteable}
'
),
{
actionText
:
this
.
commentButtonTitle
,
openOrClose
,
noteable
:
this
.
noteableDisplayName
,
},
);
}
}
return
this
.
isIssueOpen
?
'
Close issue
'
:
'
Reopen issue
'
;
return
sprintf
(
__
(
'
%{openOrClose} %{noteable}
'
),
{
openOrClose
:
capitalizeFirstCharacter
(
openOrClose
),
noteable
:
this
.
noteableDisplayName
,
},
);
},
},
actionButtonClassNames
()
{
actionButtonClassNames
()
{
return
{
return
{
'
btn-reopen
'
:
!
this
.
is
Issue
Open
,
'
btn-reopen
'
:
!
this
.
isOpen
,
'
btn-close
'
:
this
.
is
Issue
Open
,
'
btn-close
'
:
this
.
isOpen
,
'
js-note-target-close
'
:
this
.
is
Issue
Open
,
'
js-note-target-close
'
:
this
.
isOpen
,
'
js-note-target-reopen
'
:
!
this
.
is
Issue
Open
,
'
js-note-target-reopen
'
:
!
this
.
isOpen
,
};
};
},
},
markdownDocsPath
()
{
markdownDocsPath
()
{
...
@@ -138,7 +159,7 @@
...
@@ -138,7 +159,7 @@
flashContainer
:
this
.
$el
,
flashContainer
:
this
.
$el
,
data
:
{
data
:
{
note
:
{
note
:
{
noteable_type
:
constants
.
NOTEABLE_TYPE
,
noteable_type
:
this
.
noteableType
,
noteable_id
:
this
.
getNoteableData
.
id
,
noteable_id
:
this
.
getNoteableData
.
id
,
note
:
this
.
note
,
note
:
this
.
note
,
},
},
...
@@ -193,19 +214,29 @@ Please check your network connection and try again.`;
...
@@ -193,19 +214,29 @@ Please check your network connection and try again.`;
this
.
isSubmitting
=
false
;
this
.
isSubmitting
=
false
;
},
},
toggleIssueState
()
{
toggleIssueState
()
{
if
(
this
.
is
Issue
Open
)
{
if
(
this
.
isOpen
)
{
this
.
closeIssue
()
this
.
closeIssue
()
.
then
(()
=>
this
.
enableButton
())
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
.
catch
(()
=>
{
this
.
enableButton
();
this
.
enableButton
();
Flash
(
__
(
'
Something went wrong while closing the issue. Please try again later
'
));
Flash
(
sprintf
(
__
(
'
Something went wrong while closing the %{issuable}. Please try again later
'
),
{
issuable
:
this
.
noteableDisplayName
},
),
);
});
});
}
else
{
}
else
{
this
.
reopenIssue
()
this
.
reopenIssue
()
.
then
(()
=>
this
.
enableButton
())
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
.
catch
(()
=>
{
this
.
enableButton
();
this
.
enableButton
();
Flash
(
__
(
'
Something went wrong while reopening the issue. Please try again later
'
));
Flash
(
sprintf
(
__
(
'
Something went wrong while reopening the %{issuable}. Please try again later
'
),
{
issuable
:
this
.
noteableDisplayName
},
),
);
});
});
}
}
},
},
...
@@ -221,7 +252,6 @@ Please check your network connection and try again.`;
...
@@ -221,7 +252,6 @@ Please check your network connection and try again.`;
this
.
$refs
.
markdownField
.
previewMarkdown
=
false
;
this
.
$refs
.
markdownField
.
previewMarkdown
=
false
;
}
}
// reset autostave
this
.
autosave
.
reset
();
this
.
autosave
.
reset
();
},
},
setNoteType
(
type
)
{
setNoteType
(
type
)
{
...
@@ -240,10 +270,11 @@ Please check your network connection and try again.`;
...
@@ -240,10 +270,11 @@ Please check your network connection and try again.`;
},
},
initAutoSave
()
{
initAutoSave
()
{
if
(
this
.
isLoggedIn
)
{
if
(
this
.
isLoggedIn
)
{
const
noteableType
=
capitalizeFirstCharacter
(
convertToCamelCase
(
this
.
noteableType
));
this
.
autosave
=
new
Autosave
(
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
textarea
),
$
(
this
.
$refs
.
textarea
),
[
'
Note
'
,
'
Issue
'
,
this
.
getNoteableData
.
id
],
[
'
Note
'
,
noteableType
,
this
.
getNoteableData
.
id
],
'
issue
'
,
);
);
}
}
},
},
...
@@ -331,7 +362,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
...
@@ -331,7 +362,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled=
"isSubmitButtonDisabled"
:disabled=
"isSubmitButtonDisabled"
class=
"btn btn-create comment-btn js-comment-button js-comment-submit-button"
class=
"btn btn-create comment-btn js-comment-button js-comment-submit-button"
type=
"submit"
>
type=
"submit"
>
{{
commentButtonTitle
}}
{{
__
(
commentButtonTitle
)
}}
</button>
</button>
<button
<button
:disabled=
"isSubmitButtonDisabled"
:disabled=
"isSubmitButtonDisabled"
...
@@ -359,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
...
@@ -359,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<div
class=
"description"
>
<div
class=
"description"
>
<strong>
Comment
</strong>
<strong>
Comment
</strong>
<p>
<p>
Add a general comment to this
issue
.
Add a general comment to this
{{
noteableDisplayName
}}
.
</p>
</p>
</div>
</div>
</button>
</button>
...
...
app/assets/javascripts/notes/components/diff_file_header.vue
0 → 100644
View file @
059ab73b
<
script
>
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
components
:
{
ClipboardButton
,
Icon
,
},
props
:
{
diffFile
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
titleTag
()
{
return
this
.
diffFile
.
discussionPath
?
'
a
'
:
'
span
'
;
},
},
};
</
script
>
<
template
>
<div
class=
"file-header-content"
>
<div
v-if=
"diffFile.submodule"
>
<span>
<icon
name=
"archive"
/>
<strong
v-html=
"diffFile.submoduleLink"
class=
"file-title-name"
></strong>
<clipboard-button
title=
"Copy file path to clipboard"
:text=
"diffFile.submoduleLink"
/>
</span>
</div>
<template
v-else
>
<component
ref=
"titleWrapper"
:is=
"titleTag"
:href=
"diffFile.discussionPath"
>
<span
v-html=
"diffFile.blobIcon"
></span>
<span
v-if=
"diffFile.renamedFile"
>
<strong
class=
"file-title-name has-tooltip"
:title=
"diffFile.oldPath"
data-container=
"body"
>
{{
diffFile
.
oldPath
}}
</strong>
→
<strong
class=
"file-title-name has-tooltip"
:title=
"diffFile.newPath"
data-container=
"body"
>
{{
diffFile
.
newPath
}}
</strong>
</span>
<strong
v-else
class=
"file-title-name has-tooltip"
:title=
"diffFile.oldPath"
data-container=
"body"
>
{{
diffFile
.
filePath
}}
<span
v-if=
"diffFile.deletedFile"
>
deleted
</span>
</strong>
</component>
<clipboard-button
title=
"Copy file path to clipboard"
:text=
"diffFile.filePath"
/>
<small
v-if=
"diffFile.modeChanged"
ref=
"fileMode"
>
{{
diffFile
.
aMode
}}
→
{{
diffFile
.
bMode
}}
</small>
</
template
>
</div>
</template>
app/assets/javascripts/notes/components/diff_with_note.vue
0 → 100644
View file @
059ab73b
<
script
>
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
imageDiffHelper
from
'
~/image_diff/helpers/index
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
DiffFileHeader
from
'
./diff_file_header.vue
'
;
export
default
{
components
:
{
DiffFileHeader
,
},
props
:
{
discussion
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
isImageDiff
()
{
return
!
this
.
diffFile
.
text
;
},
diffFileClass
()
{
const
{
text
}
=
this
.
diffFile
;
return
text
?
'
text-file
'
:
'
js-image-file
'
;
},
diffRows
()
{
return
$
(
this
.
discussion
.
truncatedDiffLines
);
},
diffFile
()
{
return
convertObjectPropsToCamelCase
(
this
.
discussion
.
diffFile
);
},
imageDiffHtml
()
{
return
this
.
discussion
.
imageDiffHtml
;
},
},
mounted
()
{
if
(
this
.
isImageDiff
)
{
const
canCreateNote
=
false
;
const
renderCommentBadge
=
true
;
imageDiffHelper
.
initImageDiff
(
this
.
$refs
.
fileHolder
,
canCreateNote
,
renderCommentBadge
);
}
else
{
const
fileHolder
=
$
(
this
.
$refs
.
fileHolder
);
this
.
$nextTick
(()
=>
{
syntaxHighlight
(
fileHolder
);
});
}
},
methods
:
{
rowTag
(
html
)
{
return
html
.
outerHTML
?
'
tr
'
:
'
template
'
;
},
},
};
</
script
>
<
template
>
<div
ref=
"fileHolder"
class=
"diff-file file-holder"
:class=
"diffFileClass"
>
<div
class=
"js-file-title file-title file-title-flex-parent"
>
<diff-file-header
:diff-file=
"diffFile"
/>
</div>
<div
v-if=
"diffFile.text"
class=
"diff-content code js-syntax-highlight"
>
<table>
<component
:is=
"rowTag(html)"
:class=
"html.className"
v-for=
"(html, index) in diffRows"
v-html=
"html.outerHTML"
:key=
"index"
/>
<tr
class=
"notes_holder"
>
<td
class=
"notes_line"
colspan=
"2"
></td>
<td
class=
"notes_content"
>
<slot></slot>
</td>
</tr>
</table>
</div>
<div
v-else
>
<div
v-html=
"imageDiffHtml"
></div>
<slot></slot>
</div>
</div>
</
template
>
app/assets/javascripts/notes/components/discussion_counter.vue
0 → 100644
View file @
059ab73b
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
resolveSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
mrIssueSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionSvg
from
'
icons/_next_discussion.svg
'
;
import
{
pluralize
}
from
'
../../lib/utils/text_utility
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
export
default
{
directives
:
{
tooltip
,
},
computed
:
{
...
mapGetters
([
'
getUserData
'
,
'
getNoteableData
'
,
'
discussionCount
'
,
'
unresolvedDiscussions
'
,
'
resolvedDiscussionCount
'
,
]),
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
hasNextButton
()
{
return
this
.
isLoggedIn
&&
!
this
.
allResolved
;
},
countText
()
{
return
pluralize
(
'
discussion
'
,
this
.
discussionCount
);
},
allResolved
()
{
return
this
.
resolvedDiscussionCount
===
this
.
discussionCount
;
},
resolveAllDiscussionsIssuePath
()
{
return
this
.
getNoteableData
.
create_issue_to_resolve_discussions_path
;
},
firstUnresolvedDiscussionId
()
{
const
item
=
this
.
unresolvedDiscussions
[
0
]
||
{};
return
item
.
id
;
},
},
created
()
{
this
.
resolveSvg
=
resolveSvg
;
this
.
resolvedSvg
=
resolvedSvg
;
this
.
mrIssueSvg
=
mrIssueSvg
;
this
.
nextDiscussionSvg
=
nextDiscussionSvg
;
},
methods
:
{
jumpToFirstDiscussion
()
{
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
this
.
firstUnresolvedDiscussionId
}
"]`
);
const
activeTab
=
window
.
mrTabs
.
currentAction
;
if
(
activeTab
===
'
commits
'
||
activeTab
===
'
pipelines
'
)
{
window
.
mrTabs
.
activateTab
(
'
show
'
);
}
if
(
el
)
{
scrollToElement
(
el
);
}
},
},
};
</
script
>
<
template
>
<div
class=
"line-resolve-all-container prepend-top-10"
>
<div>
<div
v-if=
"discussionCount > 0"
:class=
"
{ 'has-next-btn': hasNextButton }"
class="line-resolve-all">
<span
:class=
"
{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
type="button">
<span
v-if=
"allResolved"
v-html=
"resolvedSvg"
></span>
<span
v-else
v-html=
"resolveSvg"
></span>
</span>
<span
class=
".line-resolve-text"
>
{{
resolvedDiscussionCount
}}
/
{{
discussionCount
}}
{{
countText
}}
resolved
</span>
</div>
<div
v-if=
"resolveAllDiscussionsIssuePath && !allResolved"
class=
"btn-group"
role=
"group"
>
<a
:href=
"resolveAllDiscussionsIssuePath"
v-tooltip
title=
"Resolve all discussions in new issue"
data-container=
"body"
class=
"new-issue-for-discussion btn btn-default discussion-create-issue-btn"
>
<span
v-html=
"mrIssueSvg"
></span>
</a>
</div>
<div
v-if=
"isLoggedIn && !allResolved"
class=
"btn-group"
role=
"group"
>
<button
@
click=
"jumpToFirstDiscussion"
v-tooltip
title=
"Jump to first unresolved discussion"
data-container=
"body"
class=
"btn btn-default discussion-next-btn"
>
<span
v-html=
"nextDiscussionSvg"
></span>
</button>
</div>
</div>
</div>
</
template
>
app/assets/javascripts/notes/components/note_actions.vue
View file @
059ab73b
...
@@ -4,6 +4,8 @@
...
@@ -4,6 +4,8 @@
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
editSvg
from
'
icons/_icon_pencil.svg
'
;
import
editSvg
from
'
icons/_icon_pencil.svg
'
;
import
resolveDiscussionSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedDiscussionSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
ellipsisSvg
from
'
icons/_ellipsis_v.svg
'
;
import
ellipsisSvg
from
'
icons/_ellipsis_v.svg
'
;
import
loadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
loadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
...
@@ -42,6 +44,26 @@
...
@@ -42,6 +44,26 @@
type
:
Boolean
,
type
:
Boolean
,
required
:
true
,
required
:
true
,
},
},
resolvable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isResolved
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isResolving
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
resolvedBy
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
canReportAsAbuse
:
{
canReportAsAbuse
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
true
,
required
:
true
,
...
@@ -63,6 +85,15 @@
...
@@ -63,6 +85,15 @@
currentUserId
()
{
currentUserId
()
{
return
this
.
getUserDataByProp
(
'
id
'
);
return
this
.
getUserDataByProp
(
'
id
'
);
},
},
resolveButtonTitle
()
{
let
title
=
'
Mark as resolved
'
;
if
(
this
.
resolvedBy
)
{
title
=
`Resolved by
${
this
.
resolvedBy
.
name
}
`
;
}
return
title
;
},
},
},
created
()
{
created
()
{
this
.
emojiSmiling
=
emojiSmiling
;
this
.
emojiSmiling
=
emojiSmiling
;
...
@@ -70,6 +101,8 @@
...
@@ -70,6 +101,8 @@
this
.
emojiSmiley
=
emojiSmiley
;
this
.
emojiSmiley
=
emojiSmiley
;
this
.
editSvg
=
editSvg
;
this
.
editSvg
=
editSvg
;
this
.
ellipsisSvg
=
ellipsisSvg
;
this
.
ellipsisSvg
=
ellipsisSvg
;
this
.
resolveDiscussionSvg
=
resolveDiscussionSvg
;
this
.
resolvedDiscussionSvg
=
resolvedDiscussionSvg
;
},
},
methods
:
{
methods
:
{
onEdit
()
{
onEdit
()
{
...
@@ -78,6 +111,9 @@
...
@@ -78,6 +111,9 @@
onDelete
()
{
onDelete
()
{
this
.
$emit
(
'
handleDelete
'
);
this
.
$emit
(
'
handleDelete
'
);
},
},
onResolve
()
{
this
.
$emit
(
'
handleResolve
'
);
},
},
},
};
};
</
script
>
</
script
>
...
@@ -89,6 +125,31 @@
...
@@ -89,6 +125,31 @@
class=
"note-role user-access-role"
>
class=
"note-role user-access-role"
>
{{
accessLevel
}}
{{
accessLevel
}}
</span>
</span>
<div
v-if=
"resolvable"
class=
"note-actions-item"
>
<button
v-tooltip
@
click=
"onResolve"
:class=
"
{ 'is-disabled': !resolvable, 'is-active': isResolved }"
:title="resolveButtonTitle"
:aria-label="resolveButtonTitle"
type="button"
class="line-resolve-btn note-action-button">
<template
v-if=
"!isResolving"
>
<div
v-if=
"isResolved"
v-html=
"resolvedDiscussionSvg"
></div>
<div
v-else
v-html=
"resolveDiscussionSvg"
></div>
</
template
>
<loading-icon
v-else
:inline=
"true"
/>
</button>
</div>
<div
<div
v-if=
"canAddAwardEmoji"
v-if=
"canAddAwardEmoji"
class=
"note-actions-item"
>
class=
"note-actions-item"
>
...
...
app/assets/javascripts/notes/components/note_body.vue
View file @
059ab73b
...
@@ -41,7 +41,7 @@
...
@@ -41,7 +41,7 @@
this
.
initTaskList
();
this
.
initTaskList
();
if
(
this
.
isEditing
)
{
if
(
this
.
isEditing
)
{
this
.
initAutoSave
();
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
}
}
},
},
updated
()
{
updated
()
{
...
@@ -50,7 +50,7 @@
...
@@ -50,7 +50,7 @@
if
(
this
.
isEditing
)
{
if
(
this
.
isEditing
)
{
if
(
!
this
.
autosave
)
{
if
(
!
this
.
autosave
)
{
this
.
initAutoSave
();
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
}
else
{
}
else
{
this
.
setAutoSave
();
this
.
setAutoSave
();
}
}
...
...
app/assets/javascripts/notes/components/note_form.vue
View file @
059ab73b
<
script
>
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
export
default
{
export
default
{
name
:
'
IssueNoteForm
'
,
name
:
'
IssueNoteForm
'
,
...
@@ -13,6 +14,7 @@
...
@@ -13,6 +14,7 @@
},
},
mixins
:
[
mixins
:
[
issuableStateMixin
,
issuableStateMixin
,
resolvable
,
],
],
props
:
{
props
:
{
noteBody
:
{
noteBody
:
{
...
@@ -30,7 +32,7 @@
...
@@ -30,7 +32,7 @@
required
:
false
,
required
:
false
,
default
:
'
Save comment
'
,
default
:
'
Save comment
'
,
},
},
discussion
:
{
note
:
{
type
:
Object
,
type
:
Object
,
required
:
false
,
required
:
false
,
default
:
()
=>
({}),
default
:
()
=>
({}),
...
@@ -42,9 +44,11 @@
...
@@ -42,9 +44,11 @@
},
},
data
()
{
data
()
{
return
{
return
{
note
:
this
.
noteBody
,
updatedNoteBody
:
this
.
noteBody
,
conflictWhileEditing
:
false
,
conflictWhileEditing
:
false
,
isSubmitting
:
false
,
isSubmitting
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
};
};
},
},
computed
:
{
computed
:
{
...
@@ -71,13 +75,13 @@
...
@@ -71,13 +75,13 @@
return
this
.
getUserDataByProp
(
'
id
'
);
return
this
.
getUserDataByProp
(
'
id
'
);
},
},
isDisabled
()
{
isDisabled
()
{
return
!
this
.
note
.
length
||
this
.
isSubmitting
;
return
!
this
.
updatedNoteBody
.
length
||
this
.
isSubmitting
;
},
},
},
},
watch
:
{
watch
:
{
noteBody
()
{
noteBody
()
{
if
(
this
.
note
===
this
.
noteBody
)
{
if
(
this
.
updatedNoteBody
===
this
.
noteBody
)
{
this
.
note
=
this
.
noteBody
;
this
.
updatedNoteBody
=
this
.
noteBody
;
}
else
{
}
else
{
this
.
conflictWhileEditing
=
true
;
this
.
conflictWhileEditing
=
true
;
}
}
...
@@ -87,16 +91,24 @@
...
@@ -87,16 +91,24 @@
this
.
$refs
.
textarea
.
focus
();
this
.
$refs
.
textarea
.
focus
();
},
},
methods
:
{
methods
:
{
handleUpdate
()
{
...
mapActions
([
'
toggleResolveNote
'
,
]),
handleUpdate
(
shouldResolve
)
{
const
beforeSubmitDiscussionState
=
this
.
discussionResolved
;
this
.
isSubmitting
=
true
;
this
.
isSubmitting
=
true
;
this
.
$emit
(
'
handleFormUpdate
'
,
this
.
note
,
this
.
$refs
.
editNoteForm
,
()
=>
{
this
.
$emit
(
'
handleFormUpdate
'
,
this
.
updatedNoteBody
,
this
.
$refs
.
editNoteForm
,
()
=>
{
this
.
isSubmitting
=
false
;
this
.
isSubmitting
=
false
;
if
(
shouldResolve
)
{
this
.
resolveHandler
(
beforeSubmitDiscussionState
);
}
});
});
},
},
editMyLastNote
()
{
editMyLastNote
()
{
if
(
this
.
note
===
''
)
{
if
(
this
.
updatedNoteBody
===
''
)
{
const
lastNoteInDiscussion
=
this
.
getDiscussionLastNote
(
this
.
discussion
);
const
lastNoteInDiscussion
=
this
.
getDiscussionLastNote
(
this
.
updatedNoteBody
);
if
(
lastNoteInDiscussion
)
{
if
(
lastNoteInDiscussion
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
...
@@ -107,7 +119,7 @@
...
@@ -107,7 +119,7 @@
},
},
cancelHandler
(
shouldConfirm
=
false
)
{
cancelHandler
(
shouldConfirm
=
false
)
{
// Sends information about confirm message and if the textarea has changed
// Sends information about confirm message and if the textarea has changed
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
this
.
noteBody
!==
this
.
note
);
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
this
.
noteBody
!==
this
.
updatedNoteBody
);
},
},
},
},
};
};
...
@@ -150,7 +162,7 @@
...
@@ -150,7 +162,7 @@
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
:data-supports-quick-actions=
"!isEditing"
:data-supports-quick-actions=
"!isEditing"
aria-label=
"Description"
aria-label=
"Description"
v-model=
"
note
"
v-model=
"
updatedNoteBody
"
ref=
"textarea"
ref=
"textarea"
slot=
"textarea"
slot=
"textarea"
placeholder=
"Write a comment or drag your files here..."
placeholder=
"Write a comment or drag your files here..."
...
@@ -168,6 +180,13 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
...
@@ -168,6 +180,13 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
class=
"js-vue-issue-save btn btn-save"
>
class=
"js-vue-issue-save btn btn-save"
>
{{
saveButtonTitle
}}
{{
saveButtonTitle
}}
</button>
</button>
<button
v-if=
"note.resolvable"
@
click.prevent=
"handleUpdate(true)"
class=
"btn btn-nr btn-default append-right-10 js-comment-resolve-button"
>
{{
resolveButtonTitle
}}
</button>
<button
<button
@
click=
"cancelHandler()"
@
click=
"cancelHandler()"
class=
"btn btn-cancel note-edit-cancel"
class=
"btn btn-cancel note-edit-cancel"
...
...
app/assets/javascripts/notes/components/note_header.vue
View file @
059ab73b
...
@@ -34,15 +34,15 @@
...
@@ -34,15 +34,15 @@
required
:
false
,
required
:
false
,
default
:
false
,
default
:
false
,
},
},
expanded
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
},
data
()
{
return
{
isExpanded
:
true
,
};
},
},
computed
:
{
computed
:
{
toggleChevronClass
()
{
toggleChevronClass
()
{
return
this
.
isE
xpanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
return
this
.
e
xpanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
},
},
noteTimestampLink
()
{
noteTimestampLink
()
{
return
`#note_
${
this
.
noteId
}
`
;
return
`#note_
${
this
.
noteId
}
`
;
...
@@ -53,7 +53,6 @@
...
@@ -53,7 +53,6 @@
'
setTargetNoteHash
'
,
'
setTargetNoteHash
'
,
]),
]),
handleToggle
()
{
handleToggle
()
{
this
.
isExpanded
=
!
this
.
isExpanded
;
this
.
$emit
(
'
toggleHandler
'
);
this
.
$emit
(
'
toggleHandler
'
);
},
},
updateTargetNoteHash
()
{
updateTargetNoteHash
()
{
...
...
app/assets/javascripts/notes/components/noteable_discussion.vue
View file @
059ab73b
<
script
>
<
script
>
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
resolveDiscussionsSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionsSvg
from
'
icons/_next_discussion.svg
'
;
import
Flash
from
'
../../flash
'
;
import
Flash
from
'
../../flash
'
;
import
{
SYSTEM_NOTE
}
from
'
../constants
'
;
import
{
SYSTEM_NOTE
}
from
'
../constants
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
...
@@ -8,13 +10,19 @@
...
@@ -8,13 +10,19 @@
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
diffWithNote
from
'
./diff_with_note.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
autosave
from
'
../mixins/autosave
'
;
import
autosave
from
'
../mixins/autosave
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
export
default
{
export
default
{
components
:
{
components
:
{
noteableNote
,
noteableNote
,
diffWithNote
,
userAvatarLink
,
userAvatarLink
,
noteHeader
,
noteHeader
,
noteSignedOutWidget
,
noteSignedOutWidget
,
...
@@ -23,8 +31,13 @@
...
@@ -23,8 +31,13 @@
placeholderNote
,
placeholderNote
,
placeholderSystemNote
,
placeholderSystemNote
,
},
},
directives
:
{
tooltip
,
},
mixins
:
[
mixins
:
[
autosave
,
autosave
,
noteable
,
resolvable
,
],
],
props
:
{
props
:
{
note
:
{
note
:
{
...
@@ -35,14 +48,25 @@
...
@@ -35,14 +48,25 @@
data
()
{
data
()
{
return
{
return
{
isReplying
:
false
,
isReplying
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
};
};
},
},
computed
:
{
computed
:
{
...
mapGetters
([
...
mapGetters
([
'
getNoteableData
'
,
'
getNoteableData
'
,
'
discussionCount
'
,
'
resolvedDiscussionCount
'
,
'
unresolvedDiscussions
'
,
]),
]),
discussion
()
{
discussion
()
{
return
this
.
note
.
notes
[
0
];
return
{
...
this
.
note
.
notes
[
0
],
truncatedDiffLines
:
this
.
note
.
truncated_diff_lines
,
diffFile
:
this
.
note
.
diff_file
,
diffDiscussion
:
this
.
note
.
diff_discussion
,
imageDiffHtml
:
this
.
note
.
image_diff_html
,
};
},
},
author
()
{
author
()
{
return
this
.
discussion
.
author
;
return
this
.
discussion
.
author
;
...
@@ -71,26 +95,40 @@
...
@@ -71,26 +95,40 @@
return
null
;
return
null
;
},
},
hasUnresolvedDiscussion
()
{
return
this
.
unresolvedDiscussions
.
length
>
0
;
},
wrapperComponent
()
{
return
(
this
.
discussion
.
diffDiscussion
&&
this
.
discussion
.
diffFile
)
?
diffWithNote
:
'
div
'
;
},
wrapperClass
()
{
return
this
.
isDiffDiscussion
?
''
:
'
panel panel-default
'
;
},
},
},
mounted
()
{
mounted
()
{
if
(
this
.
isReplying
)
{
if
(
this
.
isReplying
)
{
this
.
initAutoSave
();
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
}
}
},
},
updated
()
{
updated
()
{
if
(
this
.
isReplying
)
{
if
(
this
.
isReplying
)
{
if
(
!
this
.
autosave
)
{
if
(
!
this
.
autosave
)
{
this
.
initAutoSave
();
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
}
else
{
}
else
{
this
.
setAutoSave
();
this
.
setAutoSave
();
}
}
}
}
},
},
created
()
{
this
.
resolveDiscussionsSvg
=
resolveDiscussionsSvg
;
this
.
nextDiscussionsSvg
=
nextDiscussionsSvg
;
},
methods
:
{
methods
:
{
...
mapActions
([
...
mapActions
([
'
saveNote
'
,
'
saveNote
'
,
'
toggleDiscussion
'
,
'
toggleDiscussion
'
,
'
removePlaceholderNotes
'
,
'
removePlaceholderNotes
'
,
'
toggleResolveNote
'
,
]),
]),
componentName
(
note
)
{
componentName
(
note
)
{
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
isPlaceholderNote
)
{
...
@@ -103,7 +141,7 @@
...
@@ -103,7 +141,7 @@
return
noteableNote
;
return
noteableNote
;
},
},
componentData
(
note
)
{
componentData
(
note
)
{
return
note
.
isPlaceholderNote
?
note
.
notes
[
0
]
:
note
;
return
note
.
isPlaceholderNote
?
this
.
note
.
notes
[
0
]
:
note
;
},
},
toggleDiscussionHandler
()
{
toggleDiscussionHandler
()
{
this
.
toggleDiscussion
({
discussionId
:
this
.
note
.
id
});
this
.
toggleDiscussion
({
discussionId
:
this
.
note
.
id
});
...
@@ -128,7 +166,7 @@
...
@@ -128,7 +166,7 @@
flashContainer
:
this
.
$el
,
flashContainer
:
this
.
$el
,
data
:
{
data
:
{
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
target_type
:
'
issue
'
,
target_type
:
this
.
noteableType
,
target_id
:
this
.
discussion
.
noteable_id
,
target_id
:
this
.
discussion
.
noteable_id
,
note
:
{
note
:
noteText
},
note
:
{
note
:
noteText
},
},
},
...
@@ -152,12 +190,27 @@ Please check your network connection and try again.`;
...
@@ -152,12 +190,27 @@ Please check your network connection and try again.`;
});
});
});
});
},
},
jumpToDiscussion
()
{
const
unresolvedIds
=
this
.
unresolvedDiscussions
.
map
(
d
=>
d
.
id
);
const
index
=
unresolvedIds
.
indexOf
(
this
.
note
.
id
);
if
(
index
>=
0
&&
index
!==
unresolvedIds
.
length
)
{
const
nextId
=
unresolvedIds
[
index
+
1
];
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
nextId
}
"]`
);
if
(
el
)
{
scrollToElement
(
el
);
}
}
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
<li
class=
"note note-discussion timeline-entry"
>
<li
:data-discussion-id=
"note.id"
class=
"note note-discussion timeline-entry"
>
<div
class=
"timeline-entry-inner"
>
<div
class=
"timeline-entry-inner"
>
<div
class=
"timeline-icon"
>
<div
class=
"timeline-icon"
>
<user-avatar-link
<user-avatar-link
...
@@ -175,6 +228,7 @@ Please check your network connection and try again.`;
...
@@ -175,6 +228,7 @@ Please check your network connection and try again.`;
:created-at=
"discussion.created_at"
:created-at=
"discussion.created_at"
:note-id=
"discussion.id"
:note-id=
"discussion.id"
:include-toggle=
"true"
:include-toggle=
"true"
:expanded=
"note.expanded"
@
toggleHandler=
"toggleDiscussionHandler"
@
toggleHandler=
"toggleDiscussionHandler"
action-text=
"started a discussion"
action-text=
"started a discussion"
class=
"discussion"
class=
"discussion"
...
@@ -187,11 +241,14 @@ Please check your network connection and try again.`;
...
@@ -187,11 +241,14 @@ Please check your network connection and try again.`;
class-name=
"discussion-headline-light js-discussion-headline"
class-name=
"discussion-headline-light js-discussion-headline"
/>
/>
</div>
</div>
</div>
<div
<div
v-if=
"note.expanded"
v-if=
"note.expanded"
class=
"discussion-body"
>
class=
"discussion-body"
>
<div
class=
"panel panel-default"
>
<component
:is=
"wrapperComponent"
:discussion=
"discussion"
:class=
"wrapperClass"
>
<div
class=
"discussion-notes"
>
<div
class=
"discussion-notes"
>
<ul
class=
"notes"
>
<ul
class=
"notes"
>
<component
<component
...
@@ -204,26 +261,83 @@ Please check your network connection and try again.`;
...
@@ -204,26 +261,83 @@ Please check your network connection and try again.`;
<div
<div
:class=
"
{ 'is-replying': isReplying }"
:class=
"
{ 'is-replying': isReplying }"
class="discussion-reply-holder">
class="discussion-reply-holder">
<template
v-if=
"!isReplying && canReply"
>
<div
class=
"btn-group-justified discussion-with-resolve-btn"
role=
"group"
>
<div
class=
"btn-group"
role=
"group"
>
<button
<button
v-if=
"canReply && !isReplying"
@
click=
"showReplyForm"
@
click=
"showReplyForm"
type=
"button"
type=
"button"
class=
"js-vue-discussion-reply btn btn-text-field"
class=
"js-vue-discussion-reply btn btn-text-field"
title=
"Add a reply"
>
title=
"Add a reply"
>
Reply...
</button>
Reply...
</div>
<div
v-if=
"note.resolvable"
class=
"btn-group"
role=
"group"
>
<button
@
click=
"resolveHandler()"
type=
"button"
class=
"btn btn-default"
>
<i
v-if=
"isResolving"
aria-hidden=
"true"
class=
"fa fa-spinner fa-spin"
></i>
{{
resolveButtonTitle
}}
</button>
</button>
</div>
<div
class=
"btn-group discussion-actions"
role=
"group"
>
<div
v-if=
"note.resolvable && !discussionResolved"
class=
"btn-group"
role=
"group"
>
<a
:href=
"note.resolve_with_issue_path"
v-tooltip
class=
"new-issue-for-discussion btn
btn-default discussion-create-issue-btn"
title=
"Resolve this discussion in a new issue"
data-container=
"body"
>
<span
v-html=
"resolveDiscussionsSvg"
></span>
</a>
</div>
<div
v-if=
"hasUnresolvedDiscussion"
class=
"btn-group"
role=
"group"
>
<button
@
click=
"jumpToDiscussion"
v-tooltip
class=
"btn btn-default discussion-next-btn"
title=
"Jump to next unresolved discussion"
data-container=
"body"
>
<span
v-html=
"nextDiscussionsSvg"
></span>
</button>
</div>
</div>
</div>
</
template
>
<note-form
<note-form
v-if=
"isReplying"
v-if=
"isReplying"
save-button-title=
"Comment"
save-button-title=
"Comment"
:discussion
=
"note"
:note
=
"note"
:is-editing=
"false"
:is-editing=
"false"
@
handleFormUpdate=
"saveReply"
@
handleFormUpdate=
"saveReply"
@
cancelFormEdition=
"cancelReplyForm"
@
cancelFormEdition=
"cancelReplyForm"
ref=
"noteForm"
ref=
"noteForm"
/>
/>
<note-signed-out-widget
v-if=
"!canReply"
/>
<note-signed-out-widget
v-if=
"!canReply"
/>
</div>
</div>
</div>
</div>
</component>
</div>
</div>
</div>
</div>
</div>
</div>
...
...
app/assets/javascripts/notes/components/noteable_note.vue
View file @
059ab73b
...
@@ -7,6 +7,8 @@
...
@@ -7,6 +7,8 @@
import
noteActions
from
'
./note_actions.vue
'
;
import
noteActions
from
'
./note_actions.vue
'
;
import
noteBody
from
'
./note_body.vue
'
;
import
noteBody
from
'
./note_body.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
export
default
{
export
default
{
components
:
{
components
:
{
...
@@ -15,6 +17,10 @@
...
@@ -15,6 +17,10 @@
noteActions
,
noteActions
,
noteBody
,
noteBody
,
},
},
mixins
:
[
noteable
,
resolvable
,
],
props
:
{
props
:
{
note
:
{
note
:
{
type
:
Object
,
type
:
Object
,
...
@@ -26,6 +32,7 @@
...
@@ -26,6 +32,7 @@
isEditing
:
false
,
isEditing
:
false
,
isDeleting
:
false
,
isDeleting
:
false
,
isRequesting
:
false
,
isRequesting
:
false
,
isResolving
:
false
,
};
};
},
},
computed
:
{
computed
:
{
...
@@ -65,6 +72,7 @@
...
@@ -65,6 +72,7 @@
...
mapActions
([
...
mapActions
([
'
deleteNote
'
,
'
deleteNote
'
,
'
updateNote
'
,
'
updateNote
'
,
'
toggleResolveNote
'
,
'
scrollToNoteIfNeeded
'
,
'
scrollToNoteIfNeeded
'
,
]),
]),
editHandler
()
{
editHandler
()
{
...
@@ -89,7 +97,7 @@
...
@@ -89,7 +97,7 @@
const
data
=
{
const
data
=
{
endpoint
:
this
.
note
.
path
,
endpoint
:
this
.
note
.
path
,
note
:
{
note
:
{
target_type
:
'
issue
'
,
target_type
:
this
.
noteableType
,
target_id
:
this
.
note
.
noteable_id
,
target_id
:
this
.
note
.
noteable_id
,
note
:
{
note
:
noteText
},
note
:
{
note
:
noteText
},
},
},
...
@@ -134,7 +142,7 @@
...
@@ -134,7 +142,7 @@
// we need to do this to prevent noteForm inconsistent content warning
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
// this is something we intentionally do so we need to recover the content
this
.
note
.
note
=
noteText
;
this
.
note
.
note
=
noteText
;
this
.
$refs
.
noteBody
.
$refs
.
noteForm
.
note
=
noteText
;
this
.
$refs
.
noteBody
.
$refs
.
noteForm
.
note
.
note
=
noteText
;
},
},
},
},
};
};
...
@@ -171,8 +179,13 @@
...
@@ -171,8 +179,13 @@
:can-delete=
"note.current_user.can_edit"
:can-delete=
"note.current_user.can_edit"
:can-report-as-abuse=
"canReportAsAbuse"
:can-report-as-abuse=
"canReportAsAbuse"
:report-abuse-path=
"note.report_abuse_path"
:report-abuse-path=
"note.report_abuse_path"
:resolvable=
"note.resolvable"
:is-resolved=
"note.resolved"
:is-resolving=
"isResolving"
:resolved-by=
"note.resolved_by"
@
handleEdit=
"editHandler"
@
handleEdit=
"editHandler"
@
handleDelete=
"deleteHandler"
@
handleDelete=
"deleteHandler"
@
handleResolve=
"resolveHandler"
/>
/>
</div>
</div>
<note-body
<note-body
...
...
app/assets/javascripts/notes/components/notes_app.vue
View file @
059ab73b
...
@@ -11,6 +11,7 @@
...
@@ -11,6 +11,7 @@
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
skeletonLoadingContainer
from
'
../../vue_shared/components/notes/skeleton_note.vue
'
;
export
default
{
export
default
{
name
:
'
NotesApp
'
,
name
:
'
NotesApp
'
,
...
@@ -48,7 +49,24 @@
...
@@ -48,7 +49,24 @@
...
mapGetters
([
...
mapGetters
([
'
notes
'
,
'
notes
'
,
'
getNotesDataByProp
'
,
'
getNotesDataByProp
'
,
'
discussionCount
'
,
]),
]),
noteableType
()
{
// FIXME -- @fatihacet Get this from JSON data.
const
{
ISSUE_NOTEABLE_TYPE
,
MERGE_REQUEST_NOTEABLE_TYPE
}
=
constants
;
return
this
.
noteableData
.
merge_params
?
MERGE_REQUEST_NOTEABLE_TYPE
:
ISSUE_NOTEABLE_TYPE
;
},
allNotes
()
{
if
(
this
.
isLoading
)
{
const
totalNotes
=
parseInt
(
this
.
notesData
.
totalNotes
,
10
)
||
0
;
return
new
Array
(
totalNotes
).
fill
({
isSkeletonNote
:
true
,
});
}
return
this
.
notes
;
},
},
},
created
()
{
created
()
{
this
.
setNotesData
(
this
.
notesData
);
this
.
setNotesData
(
this
.
notesData
);
...
@@ -67,6 +85,10 @@
...
@@ -67,6 +85,10 @@
this
.
actionToggleAward
({
awardName
,
noteId
});
this
.
actionToggleAward
({
awardName
,
noteId
});
});
});
}
}
document
.
addEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
},
beforeDestroy
()
{
document
.
removeEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
},
},
methods
:
{
methods
:
{
...
mapActions
({
...
mapActions
({
...
@@ -81,6 +103,9 @@
...
@@ -81,6 +103,9 @@
setTargetNoteHash
:
'
setTargetNoteHash
'
,
setTargetNoteHash
:
'
setTargetNoteHash
'
,
}),
}),
getComponentName
(
note
)
{
getComponentName
(
note
)
{
if
(
note
.
isSkeletonNote
)
{
return
skeletonLoadingContainer
;
}
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
constants
.
SYSTEM_NOTE
)
{
if
(
note
.
placeholderType
===
constants
.
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
return
placeholderSystemNote
;
...
@@ -109,9 +134,14 @@
...
@@ -109,9 +134,14 @@
});
});
},
},
initPolling
()
{
initPolling
()
{
if
(
this
.
isPollingInitialized
)
{
return
;
}
this
.
setLastFetchedAt
(
this
.
getNotesDataByProp
(
'
lastFetchedAt
'
));
this
.
setLastFetchedAt
(
this
.
getNotesDataByProp
(
'
lastFetchedAt
'
));
this
.
poll
();
this
.
poll
();
this
.
isPollingInitialized
=
true
;
},
},
checkLocationHash
()
{
checkLocationHash
()
{
const
hash
=
getLocationHash
();
const
hash
=
getLocationHash
();
...
@@ -128,25 +158,20 @@
...
@@ -128,25 +158,20 @@
<
template
>
<
template
>
<div
id=
"notes"
>
<div
id=
"notes"
>
<div
v-if=
"isLoading"
class=
"js-loading loading"
>
<loading-icon
/>
</div>
<ul
<ul
v-if=
"!isLoading"
id=
"notes-list"
id=
"notes-list"
class=
"notes main-notes-list timeline"
>
class=
"notes main-notes-list timeline"
>
<component
<component
v-for=
"note in
n
otes"
v-for=
"note in
allN
otes"
:is=
"getComponentName(note)"
:is=
"getComponentName(note)"
:note=
"getComponentData(note)"
:note=
"getComponentData(note)"
:key=
"note.id"
:key=
"note.id"
/>
/>
</ul>
</ul>
<comment-form
/>
<comment-form
:noteable-type=
"noteableType"
/>
</div>
</div>
</
template
>
</
template
>
app/assets/javascripts/notes/constants.js
View file @
059ab73b
export
const
DISCUSSION_NOTE
=
'
DiscussionNote
'
;
export
const
DISCUSSION_NOTE
=
'
DiscussionNote
'
;
export
const
DIFF_NOTE
=
'
DiffNote
'
;
export
const
DISCUSSION
=
'
discussion
'
;
export
const
DISCUSSION
=
'
discussion
'
;
export
const
NOTE
=
'
note
'
;
export
const
NOTE
=
'
note
'
;
export
const
SYSTEM_NOTE
=
'
systemNote
'
;
export
const
SYSTEM_NOTE
=
'
systemNote
'
;
...
@@ -8,4 +9,7 @@ export const REOPENED = 'reopened';
...
@@ -8,4 +9,7 @@ export const REOPENED = 'reopened';
export
const
CLOSED
=
'
closed
'
;
export
const
CLOSED
=
'
closed
'
;
export
const
EMOJI_THUMBSUP
=
'
thumbsup
'
;
export
const
EMOJI_THUMBSUP
=
'
thumbsup
'
;
export
const
EMOJI_THUMBSDOWN
=
'
thumbsdown
'
;
export
const
EMOJI_THUMBSDOWN
=
'
thumbsdown
'
;
export
const
NOTEABLE_TYPE
=
'
Issue
'
;
export
const
ISSUE_NOTEABLE_TYPE
=
'
issue
'
;
export
const
MERGE_REQUEST_NOTEABLE_TYPE
=
'
merge_request
'
;
export
const
UNRESOLVE_NOTE_METHOD_NAME
=
'
delete
'
;
export
const
RESOLVE_NOTE_METHOD_NAME
=
'
post
'
;
app/assets/javascripts/notes/index.js
View file @
059ab73b
...
@@ -20,17 +20,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
...
@@ -20,17 +20,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
return
{
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
,
currentUserData
,
notesData
:
{
notesData
:
JSON
.
parse
(
notesDataset
.
notesData
),
lastFetchedAt
:
notesDataset
.
lastFetchedAt
,
discussionsPath
:
notesDataset
.
discussionsPath
,
newSessionPath
:
notesDataset
.
newSessionPath
,
registerPath
:
notesDataset
.
registerPath
,
notesPath
:
notesDataset
.
notesPath
,
markdownDocsPath
:
notesDataset
.
markdownDocsPath
,
quickActionsDocsPath
:
notesDataset
.
quickActionsDocsPath
,
closeIssuePath
:
notesDataset
.
closeIssuePath
,
reopenIssuePath
:
notesDataset
.
reopenIssuePath
,
},
};
};
},
},
render
(
createElement
)
{
render
(
createElement
)
{
...
...
app/assets/javascripts/notes/mixins/autosave.js
View file @
059ab73b
import
Autosave
from
'
../../autosave
'
;
import
Autosave
from
'
../../autosave
'
;
import
{
capitalizeFirstCharacter
}
from
'
../../lib/utils/text_utility
'
;
export
default
{
export
default
{
methods
:
{
methods
:
{
initAutoSave
()
{
initAutoSave
(
noteableType
)
{
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
noteForm
.
$refs
.
textarea
),
[
'
Note
'
,
'
Issue
'
,
this
.
note
.
id
],
'
issue
'
);
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
noteForm
.
$refs
.
textarea
),
[
'
Note
'
,
capitalizeFirstCharacter
(
noteableType
),
this
.
note
.
id
]
);
},
},
resetAutoSave
()
{
resetAutoSave
()
{
this
.
autosave
.
reset
();
this
.
autosave
.
reset
();
...
...
app/assets/javascripts/notes/mixins/noteable.js
0 → 100644
View file @
059ab73b
import
*
as
constants
from
'
../constants
'
;
export
default
{
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
noteableType
()
{
switch
(
this
.
note
.
noteable_type
)
{
case
'
MergeRequest
'
:
return
constants
.
MERGE_REQUEST_NOTEABLE_TYPE
;
case
'
Issue
'
:
return
constants
.
ISSUE_NOTEABLE_TYPE
;
default
:
return
''
;
}
},
},
};
app/assets/javascripts/notes/mixins/resolvable.js
0 → 100644
View file @
059ab73b
import
Flash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
discussionResolved
()
{
const
{
notes
,
resolved
}
=
this
.
note
;
if
(
notes
)
{
// Decide resolved state using store. Only valid for discussions.
return
notes
.
every
(
note
=>
note
.
resolved
&&
!
note
.
system
);
}
return
resolved
;
},
resolveButtonTitle
()
{
if
(
this
.
updatedNoteBody
)
{
if
(
this
.
discussionResolved
)
{
return
__
(
'
Comment and unresolve discussion
'
);
}
return
__
(
'
Comment and resolve discussion
'
);
}
return
this
.
discussionResolved
?
__
(
'
Unresolve discussion
'
)
:
__
(
'
Resolve discussion
'
);
},
},
methods
:
{
resolveHandler
(
resolvedState
=
false
)
{
this
.
isResolving
=
true
;
const
endpoint
=
this
.
note
.
resolve_path
||
`
${
this
.
note
.
path
}
/resolve`
;
const
isResolved
=
this
.
discussionResolved
||
resolvedState
;
const
discussion
=
this
.
resolveAsThread
;
this
.
toggleResolveNote
({
endpoint
,
isResolved
,
discussion
})
.
then
(()
=>
{
this
.
isResolving
=
false
;
})
.
catch
(()
=>
{
this
.
isResolving
=
false
;
const
msg
=
__
(
'
Something went wrong while resolving this discussion. Please try again.
'
);
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
});
},
},
};
app/assets/javascripts/notes/services/notes_service.js
View file @
059ab73b
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
VueResource
from
'
vue-resource
'
;
import
VueResource
from
'
vue-resource
'
;
import
*
as
constants
from
'
../constants
'
;
Vue
.
use
(
VueResource
);
Vue
.
use
(
VueResource
);
...
@@ -19,6 +20,12 @@ export default {
...
@@ -19,6 +20,12 @@ export default {
createNewNote
(
endpoint
,
data
)
{
createNewNote
(
endpoint
,
data
)
{
return
Vue
.
http
.
post
(
endpoint
,
data
,
{
emulateJSON
:
true
});
return
Vue
.
http
.
post
(
endpoint
,
data
,
{
emulateJSON
:
true
});
},
},
toggleResolveNote
(
endpoint
,
isResolved
)
{
const
{
RESOLVE_NOTE_METHOD_NAME
,
UNRESOLVE_NOTE_METHOD_NAME
}
=
constants
;
const
method
=
isResolved
?
UNRESOLVE_NOTE_METHOD_NAME
:
RESOLVE_NOTE_METHOD_NAME
;
return
Vue
.
http
[
method
](
endpoint
);
},
poll
(
data
=
{})
{
poll
(
data
=
{})
{
const
{
endpoint
,
lastFetchedAt
}
=
data
;
const
{
endpoint
,
lastFetchedAt
}
=
data
;
const
options
=
{
const
options
=
{
...
...
app/assets/javascripts/notes/stores/actions.js
View file @
059ab73b
...
@@ -61,8 +61,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
...
@@ -61,8 +61,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export
const
removePlaceholderNotes
=
({
commit
})
=>
export
const
removePlaceholderNotes
=
({
commit
})
=>
commit
(
types
.
REMOVE_PLACEHOLDER_NOTES
);
commit
(
types
.
REMOVE_PLACEHOLDER_NOTES
);
export
const
toggleResolveNote
=
({
commit
},
{
endpoint
,
isResolved
,
discussion
})
=>
service
.
toggleResolveNote
(
endpoint
,
isResolved
)
.
then
(
res
=>
res
.
json
())
.
then
((
res
)
=>
{
const
mutationType
=
discussion
?
types
.
UPDATE_DISCUSSION
:
types
.
UPDATE_NOTE
;
commit
(
mutationType
,
res
);
});
export
const
closeIssue
=
({
commit
,
dispatch
,
state
})
=>
service
export
const
closeIssue
=
({
commit
,
dispatch
,
state
})
=>
service
.
toggleIssueState
(
state
.
notesData
.
close
Issue
Path
)
.
toggleIssueState
(
state
.
notesData
.
closePath
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
.
then
((
data
)
=>
{
commit
(
types
.
CLOSE_ISSUE
);
commit
(
types
.
CLOSE_ISSUE
);
...
@@ -70,7 +79,7 @@ export const closeIssue = ({ commit, dispatch, state }) => service
...
@@ -70,7 +79,7 @@ export const closeIssue = ({ commit, dispatch, state }) => service
});
});
export
const
reopenIssue
=
({
commit
,
dispatch
,
state
})
=>
service
export
const
reopenIssue
=
({
commit
,
dispatch
,
state
})
=>
service
.
toggleIssueState
(
state
.
notesData
.
reopen
Issue
Path
)
.
toggleIssueState
(
state
.
notesData
.
reopenPath
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
.
then
((
data
)
=>
{
commit
(
types
.
REOPEN_ISSUE
);
commit
(
types
.
REOPEN_ISSUE
);
...
@@ -80,7 +89,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => service
...
@@ -80,7 +89,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => service
export
const
emitStateChangedEvent
=
({
commit
,
getters
},
data
)
=>
{
export
const
emitStateChangedEvent
=
({
commit
,
getters
},
data
)
=>
{
const
event
=
new
CustomEvent
(
'
issuable_vue_app:change
'
,
{
detail
:
{
const
event
=
new
CustomEvent
(
'
issuable_vue_app:change
'
,
{
detail
:
{
data
,
data
,
isClosed
:
getters
.
issue
State
===
constants
.
CLOSED
,
isClosed
:
getters
.
open
State
===
constants
.
CLOSED
,
}
});
}
});
document
.
dispatchEvent
(
event
);
document
.
dispatchEvent
(
event
);
...
@@ -174,7 +183,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
...
@@ -174,7 +183,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
resp
.
notes
.
forEach
((
note
)
=>
{
resp
.
notes
.
forEach
((
note
)
=>
{
if
(
notesById
[
note
.
id
])
{
if
(
notesById
[
note
.
id
])
{
commit
(
types
.
UPDATE_NOTE
,
note
);
commit
(
types
.
UPDATE_NOTE
,
note
);
}
else
if
(
note
.
type
===
constants
.
DISCUSSION_NOTE
)
{
}
else
if
(
note
.
type
===
constants
.
DISCUSSION_NOTE
||
note
.
type
===
constants
.
DIFF_NOTE
)
{
const
discussion
=
utils
.
findNoteObjectById
(
state
.
notes
,
note
.
discussion_id
);
const
discussion
=
utils
.
findNoteObjectById
(
state
.
notes
,
note
.
discussion_id
);
if
(
discussion
)
{
if
(
discussion
)
{
...
...
app/assets/javascripts/notes/stores/getters.js
View file @
059ab73b
...
@@ -8,7 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
...
@@ -8,7 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export
const
getNoteableData
=
state
=>
state
.
noteableData
;
export
const
getNoteableData
=
state
=>
state
.
noteableData
;
export
const
getNoteableDataByProp
=
state
=>
prop
=>
state
.
noteableData
[
prop
];
export
const
getNoteableDataByProp
=
state
=>
prop
=>
state
.
noteableData
[
prop
];
export
const
issue
State
=
state
=>
state
.
noteableData
.
state
;
export
const
open
State
=
state
=>
state
.
noteableData
.
state
;
export
const
getUserData
=
state
=>
state
.
userData
||
{};
export
const
getUserData
=
state
=>
state
.
userData
||
{};
export
const
getUserDataByProp
=
state
=>
prop
=>
state
.
userData
&&
state
.
userData
[
prop
];
export
const
getUserDataByProp
=
state
=>
prop
=>
state
.
userData
&&
state
.
userData
[
prop
];
...
@@ -30,3 +30,37 @@ export const getCurrentUserLastNote = state => _.flatten(
...
@@ -30,3 +30,37 @@ export const getCurrentUserLastNote = state => _.flatten(
export
const
getDiscussionLastNote
=
state
=>
discussion
=>
reverseNotes
(
discussion
.
notes
)
export
const
getDiscussionLastNote
=
state
=>
discussion
=>
reverseNotes
(
discussion
.
notes
)
.
find
(
el
=>
isLastNote
(
el
,
state
));
.
find
(
el
=>
isLastNote
(
el
,
state
));
export
const
discussionCount
=
(
state
)
=>
{
const
discussions
=
state
.
notes
.
filter
(
n
=>
!
n
.
individual_note
);
return
discussions
.
length
;
};
export
const
unresolvedDiscussions
=
(
state
,
getters
)
=>
{
const
resolvedMap
=
getters
.
resolvedDiscussionsById
;
return
state
.
notes
.
filter
(
n
=>
!
n
.
individual_note
&&
!
resolvedMap
[
n
.
id
]);
};
export
const
resolvedDiscussionsById
=
(
state
)
=>
{
const
map
=
{};
state
.
notes
.
forEach
((
n
)
=>
{
if
(
n
.
notes
)
{
const
resolved
=
n
.
notes
.
every
(
note
=>
note
.
resolved
&&
!
note
.
system
);
if
(
resolved
)
{
map
[
n
.
id
]
=
n
;
}
}
});
return
map
;
};
export
const
resolvedDiscussionCount
=
(
state
,
getters
)
=>
{
const
resolvedMap
=
getters
.
resolvedDiscussionsById
;
return
Object
.
keys
(
resolvedMap
).
length
;
};
app/assets/javascripts/notes/stores/mutation_types.js
View file @
059ab73b
...
@@ -12,6 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
...
@@ -12,6 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export
const
TOGGLE_AWARD
=
'
TOGGLE_AWARD
'
;
export
const
TOGGLE_AWARD
=
'
TOGGLE_AWARD
'
;
export
const
TOGGLE_DISCUSSION
=
'
TOGGLE_DISCUSSION
'
;
export
const
TOGGLE_DISCUSSION
=
'
TOGGLE_DISCUSSION
'
;
export
const
UPDATE_NOTE
=
'
UPDATE_NOTE
'
;
export
const
UPDATE_NOTE
=
'
UPDATE_NOTE
'
;
export
const
UPDATE_DISCUSSION
=
'
UPDATE_DISCUSSION
'
;
// Issue
// Issue
export
const
CLOSE_ISSUE
=
'
CLOSE_ISSUE
'
;
export
const
CLOSE_ISSUE
=
'
CLOSE_ISSUE
'
;
...
...
app/assets/javascripts/notes/stores/mutations.js
View file @
059ab73b
import
*
as
utils
from
'
./utils
'
;
import
*
as
utils
from
'
./utils
'
;
import
*
as
types
from
'
./mutation_types
'
;
import
*
as
types
from
'
./mutation_types
'
;
import
*
as
constants
from
'
../constants
'
;
import
*
as
constants
from
'
../constants
'
;
import
{
isInMRPage
}
from
'
../../lib/utils/common_utils
'
;
export
default
{
export
default
{
[
types
.
ADD_NEW_NOTE
](
state
,
note
)
{
[
types
.
ADD_NEW_NOTE
](
state
,
note
)
{
const
{
discussion_id
,
type
}
=
note
;
const
{
discussion_id
,
type
}
=
note
;
const
[
exists
]
=
state
.
notes
.
filter
(
n
=>
n
.
id
===
note
.
discussion_id
);
const
[
exists
]
=
state
.
notes
.
filter
(
n
=>
n
.
id
===
note
.
discussion_id
);
const
isDiscussion
=
(
type
===
constants
.
DISCUSSION_NOTE
);
if
(
!
exists
)
{
if
(
!
exists
)
{
const
noteData
=
{
const
noteData
=
{
expanded
:
true
,
expanded
:
true
,
id
:
discussion_id
,
id
:
discussion_id
,
individual_note
:
!
(
type
===
constants
.
DISCUSSION_NOTE
)
,
individual_note
:
!
isDiscussion
,
notes
:
[
note
],
notes
:
[
note
],
reply_id
:
discussion_id
,
reply_id
:
discussion_id
,
};
};
if
(
isDiscussion
&&
isInMRPage
())
{
noteData
.
resolvable
=
note
.
resolvable
;
noteData
.
resolved
=
false
;
noteData
.
resolve_path
=
note
.
resolve_path
;
noteData
.
resolve_with_issue_path
=
note
.
resolve_with_issue_path
;
}
state
.
notes
.
push
(
noteData
);
state
.
notes
.
push
(
noteData
);
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
}
}
},
},
...
@@ -25,6 +35,7 @@ export default {
...
@@ -25,6 +35,7 @@ export default {
if
(
noteObj
)
{
if
(
noteObj
)
{
noteObj
.
notes
.
push
(
note
);
noteObj
.
notes
.
push
(
note
);
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
}
}
},
},
...
@@ -41,6 +52,8 @@ export default {
...
@@ -41,6 +52,8 @@ export default {
state
.
notes
.
splice
(
state
.
notes
.
indexOf
(
noteObj
),
1
);
state
.
notes
.
splice
(
state
.
notes
.
indexOf
(
noteObj
),
1
);
}
}
}
}
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
},
},
[
types
.
REMOVE_PLACEHOLDER_NOTES
](
state
)
{
[
types
.
REMOVE_PLACEHOLDER_NOTES
](
state
)
{
...
@@ -77,15 +90,19 @@ export default {
...
@@ -77,15 +90,19 @@ export default {
const
notes
=
[];
const
notes
=
[];
notesData
.
forEach
((
note
)
=>
{
notesData
.
forEach
((
note
)
=>
{
const
nn
=
Object
.
assign
({},
note
);
// To support legacy notes, should be very rare case.
// To support legacy notes, should be very rare case.
if
(
note
.
individual_note
&&
note
.
notes
.
length
>
1
)
{
if
(
note
.
individual_note
&&
note
.
notes
.
length
>
1
)
{
note
.
notes
.
forEach
((
n
)
=>
{
note
.
notes
.
forEach
((
n
)
=>
{
const
nn
=
Object
.
assign
({},
note
);
nn
.
notes
=
[
n
];
// override notes array to only have one item to mimick individual_note
nn
.
notes
=
[
n
];
// override notes array to only have one item to mimick individual_note
notes
.
push
(
nn
);
notes
.
push
(
nn
);
});
});
}
else
{
}
else
{
notes
.
push
(
note
);
const
oldNote
=
utils
.
findNoteObjectById
(
state
.
notes
,
note
.
id
);
nn
.
expanded
=
oldNote
?
oldNote
.
expanded
:
note
.
expanded
;
notes
.
push
(
nn
);
}
}
});
});
...
@@ -134,6 +151,8 @@ export default {
...
@@ -134,6 +151,8 @@ export default {
user
:
{
id
,
name
,
username
},
user
:
{
id
,
name
,
username
},
});
});
}
}
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
},
},
[
types
.
TOGGLE_DISCUSSION
](
state
,
{
discussionId
})
{
[
types
.
TOGGLE_DISCUSSION
](
state
,
{
discussionId
})
{
...
@@ -151,6 +170,24 @@ export default {
...
@@ -151,6 +170,24 @@ export default {
const
comment
=
utils
.
findNoteObjectById
(
noteObj
.
notes
,
note
.
id
);
const
comment
=
utils
.
findNoteObjectById
(
noteObj
.
notes
,
note
.
id
);
noteObj
.
notes
.
splice
(
noteObj
.
notes
.
indexOf
(
comment
),
1
,
note
);
noteObj
.
notes
.
splice
(
noteObj
.
notes
.
indexOf
(
comment
),
1
,
note
);
}
}
// document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[
types
.
UPDATE_DISCUSSION
](
state
,
noteData
)
{
const
note
=
noteData
;
let
index
=
0
;
state
.
notes
.
forEach
((
n
,
i
)
=>
{
if
(
n
.
id
===
note
.
id
)
{
index
=
i
;
}
});
note
.
expanded
=
true
;
// override expand flag to prevent collapse
state
.
notes
.
splice
(
index
,
1
,
note
);
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
},
},
[
types
.
CLOSE_ISSUE
](
state
)
{
[
types
.
CLOSE_ISSUE
](
state
)
{
...
...
app/assets/javascripts/notes/stores/utils.js
View file @
059ab73b
...
@@ -28,4 +28,3 @@ export const getQuickActionText = (note) => {
...
@@ -28,4 +28,3 @@ export const getQuickActionText = (note) => {
export
const
hasQuickActions
=
note
=>
REGEX_QUICK_ACTIONS
.
test
(
note
);
export
const
hasQuickActions
=
note
=>
REGEX_QUICK_ACTIONS
.
test
(
note
);
export
const
stripQuickActions
=
note
=>
note
.
replace
(
REGEX_QUICK_ACTIONS
,
''
).
trim
();
export
const
stripQuickActions
=
note
=>
note
.
replace
(
REGEX_QUICK_ACTIONS
,
''
).
trim
();
app/assets/javascripts/pages/projects/merge_requests/show/index.js
View file @
059ab73b
...
@@ -22,7 +22,6 @@ document.addEventListener('DOMContentLoaded', () => {
...
@@ -22,7 +22,6 @@ document.addEventListener('DOMContentLoaded', () => {
initPipelines
();
initPipelines
();
const
mrShowNode
=
document
.
querySelector
(
'
.merge-request
'
);
const
mrShowNode
=
document
.
querySelector
(
'
.merge-request
'
);
window
.
mergeRequest
=
new
MergeRequest
({
window
.
mergeRequest
=
new
MergeRequest
({
action
:
mrShowNode
.
dataset
.
mrAction
,
action
:
mrShowNode
.
dataset
.
mrAction
,
});
});
...
...
app/assets/javascripts/vue_shared/components/clipboard_button.vue
View file @
059ab73b
<
script
>
<
script
>
import
tooltip
from
'
../directives/tooltip
'
;
/**
/**
* Falls back to the code used in `copy_to_clipboard.js`
* Falls back to the code used in `copy_to_clipboard.js`
*/
*/
import
tooltip
from
'
../directives/tooltip
'
;
export
default
{
export
default
{
name
:
'
ClipboardButton
'
,
name
:
'
ClipboardButton
'
,
...
...
app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
View file @
059ab73b
...
@@ -11,14 +11,12 @@
...
@@ -11,14 +11,12 @@
default
:
false
,
default
:
false
,
required
:
false
,
required
:
false
,
},
},
isConfidential
:
{
isConfidential
:
{
type
:
Boolean
,
type
:
Boolean
,
default
:
false
,
default
:
false
,
required
:
false
,
required
:
false
,
},
},
},
},
computed
:
{
computed
:
{
warningIcon
()
{
warningIcon
()
{
if
(
this
.
isConfidential
)
return
'
eye-slash
'
;
if
(
this
.
isConfidential
)
return
'
eye-slash
'
;
...
@@ -26,7 +24,6 @@
...
@@ -26,7 +24,6 @@
return
''
;
return
''
;
},
},
isLockedAndConfidential
()
{
isLockedAndConfidential
()
{
return
this
.
isConfidential
&&
this
.
isLocked
;
return
this
.
isConfidential
&&
this
.
isLocked
;
},
},
...
...
app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
0 → 100644
View file @
059ab73b
<
template
>
<li
class=
"timeline-entry note"
>
<div
class=
"timeline-entry-inner"
>
<div
class=
"timeline-icon"
>
</div>
<div
class=
"timeline-content"
>
<div
class=
"note-header"
></div>
<div
class=
"note-body"
>
<skeleton-loading-container
/>
</div>
</div>
</div>
</li>
</
template
>
<
script
>
import
skeletonLoadingContainer
from
'
~/vue_shared/components/skeleton_loading_container.vue
'
;
export
default
{
components
:
{
skeletonLoadingContainer
,
},
};
</
script
>
app/assets/stylesheets/pages/notes.scss
View file @
059ab73b
...
@@ -723,7 +723,7 @@ ul.notes {
...
@@ -723,7 +723,7 @@ ul.notes {
.line-resolve-all
{
.line-resolve-all
{
vertical-align
:
middle
;
vertical-align
:
middle
;
display
:
inline-block
;
display
:
inline-block
;
padding
:
5px
10px
6
px
;
padding
:
6px
10
px
;
background-color
:
$gray-light
;
background-color
:
$gray-light
;
border
:
1px
solid
$border-color
;
border
:
1px
solid
$border-color
;
border-radius
:
$border-radius-default
;
border-radius
:
$border-radius-default
;
...
...
app/controllers/concerns/issuable_actions.rb
View file @
059ab73b
...
@@ -77,6 +77,20 @@ module IssuableActions
...
@@ -77,6 +77,20 @@ module IssuableActions
render
json:
{
notice:
"
#{
quantity
}
#{
resource_name
.
pluralize
(
quantity
)
}
updated"
}
render
json:
{
notice:
"
#{
quantity
}
#{
resource_name
.
pluralize
(
quantity
)
}
updated"
}
end
end
def
discussions
notes
=
issuable
.
notes
.
inc_relations_for_view
.
includes
(
:noteable
)
.
fresh
notes
=
prepare_notes_for_rendering
(
notes
)
notes
=
notes
.
reject
{
|
n
|
n
.
cross_reference_not_visible_for?
(
current_user
)
}
discussions
=
Discussion
.
build_collection
(
notes
,
issuable
)
render
json:
DiscussionSerializer
.
new
(
project:
project
,
noteable:
issuable
,
current_user:
current_user
).
represent
(
discussions
,
context:
self
)
end
private
private
def
recaptcha_check_if_spammable
(
should_redirect
=
true
,
&
block
)
def
recaptcha_check_if_spammable
(
should_redirect
=
true
,
&
block
)
...
...
app/controllers/concerns/notes_actions.rb
View file @
059ab73b
...
@@ -22,7 +22,7 @@ module NotesActions
...
@@ -22,7 +22,7 @@ module NotesActions
notes
=
notes
.
reject
{
|
n
|
n
.
cross_reference_not_visible_for?
(
current_user
)
}
notes
=
notes
.
reject
{
|
n
|
n
.
cross_reference_not_visible_for?
(
current_user
)
}
notes_json
[
:notes
]
=
notes_json
[
:notes
]
=
if
noteable
.
discussions_rendered_on_frontend
?
if
use_note_serializer
?
note_serializer
.
represent
(
notes
)
note_serializer
.
represent
(
notes
)
else
else
notes
.
map
{
|
note
|
note_json
(
note
)
}
notes
.
map
{
|
note
|
note_json
(
note
)
}
...
@@ -95,7 +95,7 @@ module NotesActions
...
@@ -95,7 +95,7 @@ module NotesActions
if
note
.
persisted?
if
note
.
persisted?
attrs
[
:valid
]
=
true
attrs
[
:valid
]
=
true
if
noteable
.
discussions_rendered_on_frontend
?
if
use_note_serializer
?
attrs
.
merge!
(
note_serializer
.
represent
(
note
))
attrs
.
merge!
(
note_serializer
.
represent
(
note
))
else
else
attrs
.
merge!
(
attrs
.
merge!
(
...
@@ -233,4 +233,14 @@ module NotesActions
...
@@ -233,4 +233,14 @@ module NotesActions
the_project
the_project
end
end
end
end
def
use_note_serializer?
return
false
if
params
[
'html'
]
if
noteable
.
is_a?
(
MergeRequest
)
cookies
[
:vue_mr_discussions
]
==
'true'
else
noteable
.
discussions_rendered_on_frontend?
end
end
end
end
app/controllers/projects/discussions_controller.rb
View file @
059ab73b
class
Projects::DiscussionsController
<
Projects
::
ApplicationController
class
Projects::DiscussionsController
<
Projects
::
ApplicationController
include
NotesHelper
include
RendersNotes
before_action
:check_merge_requests_available!
before_action
:check_merge_requests_available!
before_action
:merge_request
before_action
:merge_request
before_action
:discussion
before_action
:discussion
...
@@ -7,22 +10,45 @@ class Projects::DiscussionsController < Projects::ApplicationController
...
@@ -7,22 +10,45 @@ class Projects::DiscussionsController < Projects::ApplicationController
def
resolve
def
resolve
Discussions
::
ResolveService
.
new
(
project
,
current_user
,
merge_request:
merge_request
).
execute
(
discussion
)
Discussions
::
ResolveService
.
new
(
project
,
current_user
,
merge_request:
merge_request
).
execute
(
discussion
)
render
json:
{
render_discussion
resolved_by:
discussion
.
resolved_by
.
try
(
:name
),
discussion_headline_html:
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
}
end
end
def
unresolve
def
unresolve
discussion
.
unresolve!
discussion
.
unresolve!
render_discussion
end
private
def
render_discussion
if
serialize_notes?
# TODO - It is not needed to serialize notes when resolving
# or unresolving discussions. We should remove this behavior
# passing a parameter to DiscussionEntity to return an empty array
# for notes.
# Check issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/42853
prepare_notes_for_rendering
(
discussion
.
notes
,
merge_request
)
render_json_with_discussions_serializer
else
render_json_with_html
end
end
def
render_json_with_discussions_serializer
render
json:
DiscussionSerializer
.
new
(
project:
project
,
noteable:
discussion
.
noteable
,
current_user:
current_user
)
.
represent
(
discussion
,
context:
self
)
end
# Legacy method used to render discussions notes when not using Vue on views.
def
render_json_with_html
render
json:
{
render
json:
{
resolved_by:
discussion
.
resolved_by
.
try
(
:name
),
discussion_headline_html:
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
discussion_headline_html:
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
}
}
end
end
private
def
merge_request
def
merge_request
@merge_request
||=
MergeRequestsFinder
.
new
(
current_user
,
project_id:
@project
.
id
).
find_by!
(
iid:
params
[
:merge_request_id
])
@merge_request
||=
MergeRequestsFinder
.
new
(
current_user
,
project_id:
@project
.
id
).
find_by!
(
iid:
params
[
:merge_request_id
])
end
end
...
...
app/controllers/projects/issues_controller.rb
View file @
059ab73b
...
@@ -60,20 +60,6 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -60,20 +60,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with
(
@issue
)
respond_with
(
@issue
)
end
end
def
discussions
notes
=
@issue
.
notes
.
inc_relations_for_view
.
includes
(
:noteable
)
.
fresh
notes
=
prepare_notes_for_rendering
(
notes
)
notes
=
notes
.
reject
{
|
n
|
n
.
cross_reference_not_visible_for?
(
current_user
)
}
discussions
=
Discussion
.
build_collection
(
notes
,
@issue
)
render
json:
DiscussionSerializer
.
new
(
project:
@project
,
noteable:
@issue
,
current_user:
current_user
).
represent
(
discussions
)
end
def
create
def
create
create_params
=
issue_params
.
merge
(
spammable_params
).
merge
(
create_params
=
issue_params
.
merge
(
spammable_params
).
merge
(
merge_request_to_resolve_discussions_of:
params
[
:merge_request_to_resolve_discussions_of
],
merge_request_to_resolve_discussions_of:
params
[
:merge_request_to_resolve_discussions_of
],
...
...
app/controllers/projects/notes_controller.rb
View file @
059ab73b
class
Projects::NotesController
<
Projects
::
ApplicationController
class
Projects::NotesController
<
Projects
::
ApplicationController
include
NotesActions
include
NotesActions
include
NotesHelper
include
ToggleAwardEmoji
include
ToggleAwardEmoji
before_action
:whitelist_query_limiting
,
only:
[
:create
]
before_action
:whitelist_query_limiting
,
only:
[
:create
]
...
@@ -38,11 +39,15 @@ class Projects::NotesController < Projects::ApplicationController
...
@@ -38,11 +39,15 @@ class Projects::NotesController < Projects::ApplicationController
discussion
=
note
.
discussion
discussion
=
note
.
discussion
if
serialize_notes?
render_json_with_notes_serializer
else
render
json:
{
render
json:
{
resolved_by:
note
.
resolved_by
.
try
(
:name
),
resolved_by:
note
.
resolved_by
.
try
(
:name
),
discussion_headline_html:
(
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
if
discussion
)
discussion_headline_html:
(
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
if
discussion
)
}
}
end
end
end
def
unresolve
def
unresolve
return
render_404
unless
note
.
resolvable?
return
render_404
unless
note
.
resolvable?
...
@@ -51,16 +56,27 @@ class Projects::NotesController < Projects::ApplicationController
...
@@ -51,16 +56,27 @@ class Projects::NotesController < Projects::ApplicationController
discussion
=
note
.
discussion
discussion
=
note
.
discussion
if
serialize_notes?
render_json_with_notes_serializer
else
render
json:
{
render
json:
{
discussion_headline_html:
(
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
if
discussion
)
discussion_headline_html:
(
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
if
discussion
)
}
}
end
end
end
private
private
def
render_json_with_notes_serializer
Notes
::
RenderService
.
new
(
current_user
).
execute
([
note
],
project
)
render
json:
note_serializer
.
represent
(
note
)
end
def
note
def
note
@note
||=
@project
.
notes
.
find
(
params
[
:id
])
@note
||=
@project
.
notes
.
find
(
params
[
:id
])
end
end
alias_method
:awardable
,
:note
alias_method
:awardable
,
:note
def
finder_params
def
finder_params
...
...
app/helpers/notes_helper.rb
View file @
059ab73b
...
@@ -151,7 +151,38 @@ module NotesHelper
...
@@ -151,7 +151,38 @@ module NotesHelper
}
}
end
end
def
notes_data
(
issuable
)
discussions_path
=
if
issuable
.
is_a?
(
Issue
)
discussions_project_issue_path
(
@project
,
issuable
,
format: :json
)
else
discussions_project_merge_request_path
(
@project
,
issuable
,
format: :json
)
end
{
discussionsPath:
discussions_path
,
registerPath:
new_session_path
(
:user
,
redirect_to_referer:
'yes'
,
anchor:
'register-pane'
),
newSessionPath:
new_session_path
(
:user
,
redirect_to_referer:
'yes'
),
markdownDocsPath:
help_page_path
(
'user/markdown'
),
quickActionsDocsPath:
help_page_path
(
'user/project/quick_actions'
),
closePath:
close_issuable_path
(
issuable
),
reopenPath:
reopen_issuable_path
(
issuable
),
notesPath:
notes_url
,
totalNotes:
issuable
.
discussions
.
length
,
lastFetchedAt:
Time
.
now
}.
to_json
end
def
discussion_resolved_intro
(
discussion
)
def
discussion_resolved_intro
(
discussion
)
discussion
.
resolved_by_push?
?
'Automatically resolved'
:
'Resolved'
discussion
.
resolved_by_push?
?
'Automatically resolved'
:
'Resolved'
end
end
def
has_vue_discussions_cookie?
cookies
[
:vue_mr_discussions
]
==
'true'
end
def
serialize_notes?
has_vue_discussions_cookie?
&&
!
params
[
'html'
]
end
end
end
app/models/note.rb
View file @
059ab73b
...
@@ -133,6 +133,7 @@ class Note < ActiveRecord::Base
...
@@ -133,6 +133,7 @@ class Note < ActiveRecord::Base
def
find_discussion
(
discussion_id
)
def
find_discussion
(
discussion_id
)
notes
=
where
(
discussion_id:
discussion_id
).
fresh
.
to_a
notes
=
where
(
discussion_id:
discussion_id
).
fresh
.
to_a
return
if
notes
.
empty?
return
if
notes
.
empty?
Discussion
.
build
(
notes
)
Discussion
.
build
(
notes
)
...
...
app/serializers/diff_file_entity.rb
0 → 100644
View file @
059ab73b
class
DiffFileEntity
<
Grape
::
Entity
include
DiffHelper
include
SubmoduleHelper
include
BlobHelper
include
IconsHelper
include
ActionView
::
Helpers
::
TagHelper
expose
:submodule?
,
as: :submodule
expose
:submodule_link
do
|
diff_file
|
submodule_links
(
diff_file
.
blob
,
diff_file
.
content_sha
,
diff_file
.
repository
).
first
end
expose
:blob_path
do
|
diff_file
|
diff_file
.
blob
.
path
end
expose
:blob_icon
do
|
diff_file
|
blob_icon
(
diff_file
.
b_mode
,
diff_file
.
file_path
)
end
expose
:file_path
expose
:deleted_file?
,
as: :deleted_file
expose
:renamed_file?
,
as: :renamed_file
expose
:old_path
expose
:new_path
expose
:mode_changed?
,
as: :mode_changed
expose
:a_mode
expose
:b_mode
expose
:text?
,
as: :text
expose
:old_path_html
do
|
diff_file
|
old_path
=
mark_inline_diffs
(
diff_file
.
old_path
,
diff_file
.
new_path
)
old_path
end
expose
:new_path_html
do
|
diff_file
|
_
,
new_path
=
mark_inline_diffs
(
diff_file
.
old_path
,
diff_file
.
new_path
)
new_path
end
end
app/serializers/discussion_entity.rb
View file @
059ab73b
...
@@ -7,4 +7,42 @@ class DiscussionEntity < Grape::Entity
...
@@ -7,4 +7,42 @@ class DiscussionEntity < Grape::Entity
expose
:notes
,
using:
NoteEntity
expose
:notes
,
using:
NoteEntity
expose
:individual_note?
,
as: :individual_note
expose
:individual_note?
,
as: :individual_note
expose
:resolvable?
,
as: :resolvable
expose
:resolved?
,
as: :resolved
expose
:resolve_path
,
if:
->
(
d
,
_
)
{
d
.
resolvable?
}
do
|
discussion
|
resolve_project_merge_request_discussion_path
(
discussion
.
project
,
discussion
.
noteable
,
discussion
.
id
)
end
expose
:resolve_with_issue_path
do
|
discussion
|
new_project_issue_path
(
discussion
.
project
,
merge_request_to_resolve_discussions_of:
discussion
.
noteable
.
iid
,
discussion_to_resolve:
discussion
.
id
)
end
expose
:diff_file
,
using:
DiffFileEntity
,
if:
->
(
d
,
_
)
{
defined?
d
.
diff_file
}
expose
:diff_discussion?
,
as: :diff_discussion
expose
:truncated_diff_lines
,
if:
->
(
d
,
_
)
{
(
defined?
d
.
diff_file
)
&&
d
.
diff_file
.
text?
}
do
|
discussion
|
options
[
:context
].
render_to_string
(
partial:
"projects/diffs/line"
,
collection:
discussion
.
truncated_diff_lines
,
as: :line
,
locals:
{
diff_file:
discussion
.
diff_file
,
discussion_expanded:
true
,
plain:
true
},
layout:
false
,
formats:
[
:html
]
)
end
expose
:image_diff_html
,
if:
->
(
d
,
_
)
{
defined?
d
.
diff_file
}
do
|
discussion
|
diff_file
=
discussion
.
diff_file
partial
=
diff_file
.
new_file?
||
diff_file
.
deleted_file?
?
'single_image_diff'
:
'replaced_image_diff'
options
[
:context
].
render_to_string
(
partial:
"projects/diffs/
#{
partial
}
"
,
locals:
{
diff_file:
diff_file
,
position:
discussion
.
position
.
to_json
,
click_to_comment:
false
},
layout:
false
,
formats:
[
:html
]
)
end
end
end
app/serializers/merge_request_widget_entity.rb
View file @
059ab73b
...
@@ -115,6 +115,14 @@ class MergeRequestWidgetEntity < IssuableEntity
...
@@ -115,6 +115,14 @@ class MergeRequestWidgetEntity < IssuableEntity
expose
:can_cherry_pick_on_current_merge_request
do
|
merge_request
|
expose
:can_cherry_pick_on_current_merge_request
do
|
merge_request
|
presenter
(
merge_request
).
can_cherry_pick_on_current_merge_request?
presenter
(
merge_request
).
can_cherry_pick_on_current_merge_request?
end
end
expose
:can_create_note
do
|
issue
|
can?
(
request
.
current_user
,
:create_note
,
issue
.
project
)
end
expose
:can_update
do
|
issue
|
can?
(
request
.
current_user
,
:update_issue
,
issue
)
end
end
end
# Paths
# Paths
...
@@ -189,6 +197,10 @@ class MergeRequestWidgetEntity < IssuableEntity
...
@@ -189,6 +197,10 @@ class MergeRequestWidgetEntity < IssuableEntity
end
end
end
end
expose
:create_note_path
do
|
merge_request
|
project_notes_path
(
merge_request
.
project
,
target_type:
'merge_request'
,
target_id:
merge_request
.
id
)
end
expose
:commit_change_content_path
do
|
merge_request
|
expose
:commit_change_content_path
do
|
merge_request
|
commit_change_content_project_merge_request_path
(
merge_request
.
project
,
merge_request
)
commit_change_content_project_merge_request_path
(
merge_request
.
project
,
merge_request
)
end
end
...
...
app/serializers/note_entity.rb
View file @
059ab73b
...
@@ -23,6 +23,10 @@ class NoteEntity < API::Entities::Note
...
@@ -23,6 +23,10 @@ class NoteEntity < API::Entities::Note
end
end
end
end
expose
:resolved?
,
as: :resolved
expose
:resolvable?
,
as: :resolvable
expose
:resolved_by
,
using:
NoteUserEntity
expose
:system_note_icon_name
,
if:
->
(
note
,
_
)
{
note
.
system?
}
do
|
note
|
expose
:system_note_icon_name
,
if:
->
(
note
,
_
)
{
note
.
system?
}
do
|
note
|
SystemNoteHelper
.
system_note_icon_name
(
note
)
SystemNoteHelper
.
system_note_icon_name
(
note
)
end
end
...
@@ -53,6 +57,14 @@ class NoteEntity < API::Entities::Note
...
@@ -53,6 +57,14 @@ class NoteEntity < API::Entities::Note
end
end
end
end
expose
:resolve_path
,
if:
->
(
note
,
_
)
{
note
.
part_of_discussion?
&&
note
.
resolvable?
}
do
|
note
|
resolve_project_merge_request_discussion_path
(
note
.
project
,
note
.
noteable
,
note
.
discussion_id
)
end
expose
:resolve_with_issue_path
,
if:
->
(
note
,
_
)
{
note
.
part_of_discussion?
&&
note
.
resolvable?
}
do
|
note
|
new_project_issue_path
(
note
.
project
,
merge_request_to_resolve_discussions_of:
note
.
noteable
.
iid
,
discussion_to_resolve:
note
.
discussion_id
)
end
expose
:attachment
,
using:
NoteAttachmentEntity
,
if:
->
(
note
,
_
)
{
note
.
attachment?
}
expose
:attachment
,
using:
NoteAttachmentEntity
,
if:
->
(
note
,
_
)
{
note
.
attachment?
}
expose
:delete_attachment_path
,
if:
->
(
note
,
_
)
{
note
.
attachment?
}
do
|
note
|
expose
:delete_attachment_path
,
if:
->
(
note
,
_
)
{
note
.
attachment?
}
do
|
note
|
delete_attachment_project_note_path
(
note
.
project
,
note
)
delete_attachment_project_note_path
(
note
.
project
,
note
)
...
...
app/views/projects/issues/_discussion.html.haml
View file @
059ab73b
...
@@ -6,14 +6,6 @@
...
@@ -6,14 +6,6 @@
=
link_to
'Close issue'
,
issue_path
(
@issue
,
issue:
{
state_event: :close
},
format:
'json'
),
data:
{
original_text:
"Close issue"
,
alternative_text:
"Comment & close issue"
},
class:
"btn btn-nr btn-close btn-comment js-note-target-close
#{
issue_button_visibility
(
@issue
,
true
)
}
"
,
title:
'Close issue'
=
link_to
'Close issue'
,
issue_path
(
@issue
,
issue:
{
state_event: :close
},
format:
'json'
),
data:
{
original_text:
"Close issue"
,
alternative_text:
"Comment & close issue"
},
class:
"btn btn-nr btn-close btn-comment js-note-target-close
#{
issue_button_visibility
(
@issue
,
true
)
}
"
,
title:
'Close issue'
%section
.js-vue-notes-event
%section
.js-vue-notes-event
#js-vue-notes
{
data:
{
discussions_path:
discussions_project_issue_path
(
@project
,
@issue
,
format: :json
),
#js-vue-notes
{
data:
{
notes_data:
notes_data
(
@issue
),
register_path:
new_session_path
(
:user
,
redirect_to_referer:
'yes'
,
anchor:
'register-pane'
),
new_session_path:
new_session_path
(
:user
,
redirect_to_referer:
'yes'
),
markdown_docs_path:
help_page_path
(
'user/markdown'
),
quick_actions_docs_path:
help_page_path
(
'user/project/quick_actions'
),
notes_path:
notes_url
,
close_issue_path:
issue_path
(
@issue
,
issue:
{
state_event: :close
},
format:
'json'
),
reopen_issue_path:
issue_path
(
@issue
,
issue:
{
state_event: :reopen
},
format:
'json'
),
last_fetched_at:
Time
.
now
.
to_i
,
noteable_data:
serialize_issuable
(
@issue
),
noteable_data:
serialize_issuable
(
@issue
),
current_user_data:
UserSerializer
.
new
.
represent
(
current_user
,
only_path:
true
).
to_json
}
}
current_user_data:
UserSerializer
.
new
.
represent
(
current_user
,
only_path:
true
).
to_json
}
}
app/views/projects/issues/show.html.haml
View file @
059ab73b
...
@@ -73,7 +73,7 @@
...
@@ -73,7 +73,7 @@
.content-block.emoji-block
.content-block.emoji-block
.row
.row
.col-sm-8.js-
issue-not
e-awards
.col-sm-8.js-
noteabl
e-awards
=
render
'award_emoji/awards_block'
,
awardable:
@issue
,
inline:
true
=
render
'award_emoji/awards_block'
,
awardable:
@issue
,
inline:
true
.col-sm-4.new-branch-col
.col-sm-4.new-branch-col
=
render
'new_branch'
unless
@issue
.
confidential?
=
render
'new_branch'
unless
@issue
.
confidential?
...
...
app/views/projects/merge_requests/show.html.haml
View file @
059ab73b
-
@gfm_form
=
true
-
@content_class
=
"limit-container-width"
unless
fluid_layout
-
@content_class
=
"limit-container-width"
unless
fluid_layout
-
add_to_breadcrumbs
"Merge Requests"
,
project_merge_requests_path
(
@project
)
-
add_to_breadcrumbs
"Merge Requests"
,
project_merge_requests_path
(
@project
)
-
breadcrumb_title
@merge_request
.
to_reference
-
breadcrumb_title
@merge_request
.
to_reference
...
@@ -7,6 +8,9 @@
...
@@ -7,6 +8,9 @@
-
content_for
:page_specific_javascripts
do
-
content_for
:page_specific_javascripts
do
=
webpack_bundle_tag
(
'common_vue'
)
=
webpack_bundle_tag
(
'common_vue'
)
-
if
has_vue_discussions_cookie?
=
webpack_bundle_tag
(
'mr_notes'
)
.merge-request
{
data:
{
mr_action:
j
(
params
[
:tab
].
presence
||
'show'
),
url:
merge_request_path
(
@merge_request
,
format: :json
),
project_path:
project_path
(
@merge_request
.
project
)
}
}
.merge-request
{
data:
{
mr_action:
j
(
params
[
:tab
].
presence
||
'show'
),
url:
merge_request_path
(
@merge_request
,
format: :json
),
project_path:
project_path
(
@merge_request
.
project
)
}
}
=
render
"projects/merge_requests/mr_title"
=
render
"projects/merge_requests/mr_title"
...
@@ -23,7 +27,7 @@
...
@@ -23,7 +27,7 @@
#js-vue-mr-widget
.mr-widget
#js-vue-mr-widget
.mr-widget
.content-block.content-block-small.emoji-list-container
.content-block.content-block-small.emoji-list-container
.js-noteable-awards
=
render
'award_emoji/awards_block'
,
awardable:
@merge_request
,
inline:
true
=
render
'award_emoji/awards_block'
,
awardable:
@merge_request
,
inline:
true
.merge-request-tabs-holder
{
class:
(
"js-tabs-affix"
unless
ENV
[
'RAILS_ENV'
]
==
'test'
)
}
.merge-request-tabs-holder
{
class:
(
"js-tabs-affix"
unless
ENV
[
'RAILS_ENV'
]
==
'test'
)
}
...
@@ -51,6 +55,10 @@
...
@@ -51,6 +55,10 @@
=
tab_link_for
@merge_request
,
:diffs
do
=
tab_link_for
@merge_request
,
:diffs
do
Changes
Changes
%span
.badge
=
@merge_request
.
diff_size
%span
.badge
=
@merge_request
.
diff_size
-
if
has_vue_discussions_cookie?
#js-vue-discussion-counter
-
else
#resolve-count-app
.line-resolve-all-container.prepend-top-10
{
"v-cloak"
=>
true
}
#resolve-count-app
.line-resolve-all-container.prepend-top-10
{
"v-cloak"
=>
true
}
%resolve-count
{
"inline-template"
=>
true
,
":logged-out"
=>
"#{current_user.nil?}"
}
%resolve-count
{
"inline-template"
=>
true
,
":logged-out"
=>
"#{current_user.nil?}"
}
%div
%div
...
@@ -71,8 +79,13 @@
...
@@ -71,8 +79,13 @@
#notes
.notes.tab-pane.voting_notes
#notes
.notes.tab-pane.voting_notes
.row
.row
%section
.col-md-12
%section
.col-md-12
.issuable-discussion
%script
.js-notes-data
{
type:
"application/json"
}=
initial_notes_data
(
true
).
to_json
.
html_safe
.issuable-discussion.js-vue-notes-event
=
render
"projects/merge_requests/discussion"
=
render
"projects/merge_requests/discussion"
-
if
has_vue_discussions_cookie?
#js-vue-mr-discussions
{
data:
{
notes_data:
notes_data
(
@merge_request
),
noteable_data:
serialize_issuable
(
@merge_request
),
current_user_data:
UserSerializer
.
new
.
represent
(
current_user
).
to_json
}
}
#commits
.commits.tab-pane
#commits
.commits.tab-pane
-# This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
...
...
app/views/shared/notes/_notes_with_form.html.haml
View file @
059ab73b
-
issuable
=
@issue
||
@merge_request
-
issuable
=
@issue
||
@merge_request
-
discussion_locked
=
issuable
&
.
discussion_locked?
-
discussion_locked
=
issuable
&
.
discussion_locked?
%ul
#notes-list
.notes.main-notes-list.timeline
-
unless
has_vue_discussions_cookie?
%ul
#notes-list
.notes.main-notes-list.timeline
=
render
"shared/notes/notes"
=
render
"shared/notes/notes"
=
render
'shared/notes/edit_form'
,
project:
@project
=
render
'shared/notes/edit_form'
,
project:
@project
-
if
can_create_note?
-
if
can_create_note?
%ul
.notes.notes-form.timeline
%ul
.notes.notes-form.timeline
{
:class
=>
(
'hidden'
if
has_vue_discussions_cookie?
)
}
%li
.timeline-entry
%li
.timeline-entry
.timeline-entry-inner
.timeline-entry-inner
.flash-container.timeline-content
.flash-container.timeline-content
...
...
config/routes/project.rb
View file @
059ab73b
...
@@ -103,6 +103,7 @@ constraints(ProjectUrlConstrainer.new) do
...
@@ -103,6 +103,7 @@ constraints(ProjectUrlConstrainer.new) do
post
:toggle_subscription
post
:toggle_subscription
post
:remove_wip
post
:remove_wip
post
:assign_related_issues
post
:assign_related_issues
get
:discussions
,
format: :json
post
:rebase
post
:rebase
scope
constraints:
{
format:
nil
},
action: :show
do
scope
constraints:
{
format:
nil
},
action: :show
do
...
...
config/webpack.config.js
View file @
059ab73b
...
@@ -52,6 +52,7 @@ function generateEntries() {
...
@@ -52,6 +52,7 @@ function generateEntries() {
help
:
'
./help/help.js
'
,
help
:
'
./help/help.js
'
,
merge_conflicts
:
'
./merge_conflicts/merge_conflicts_bundle.js
'
,
merge_conflicts
:
'
./merge_conflicts/merge_conflicts_bundle.js
'
,
monitoring
:
'
./monitoring/monitoring_bundle.js
'
,
monitoring
:
'
./monitoring/monitoring_bundle.js
'
,
mr_notes
:
'
./mr_notes/index.js
'
,
notebook_viewer
:
'
./blob/notebook_viewer.js
'
,
notebook_viewer
:
'
./blob/notebook_viewer.js
'
,
pdf_viewer
:
'
./blob/pdf_viewer.js
'
,
pdf_viewer
:
'
./blob/pdf_viewer.js
'
,
pipelines_details
:
'
./pipelines/pipeline_details_bundle.js
'
,
pipelines_details
:
'
./pipelines/pipeline_details_bundle.js
'
,
...
@@ -243,6 +244,7 @@ var config = {
...
@@ -243,6 +244,7 @@ var config = {
'
groups
'
,
'
groups
'
,
'
merge_conflicts
'
,
'
merge_conflicts
'
,
'
monitoring
'
,
'
monitoring
'
,
'
mr_notes
'
,
'
notebook_viewer
'
,
'
notebook_viewer
'
,
'
pdf_viewer
'
,
'
pdf_viewer
'
,
'
pipelines
'
,
'
pipelines
'
,
...
...
spec/controllers/projects/discussions_controller_spec.rb
View file @
059ab73b
...
@@ -71,6 +71,19 @@ describe Projects::DiscussionsController do
...
@@ -71,6 +71,19 @@ describe Projects::DiscussionsController do
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
).
to
have_gitlab_http_status
(
200
)
end
end
context
"when vue_mr_discussions cookie is present"
do
before
do
allow
(
controller
).
to
receive
(
:cookies
).
and_return
(
vue_mr_discussions:
'true'
)
end
it
"renders discussion with serializer"
do
expect_any_instance_of
(
DiscussionSerializer
).
to
receive
(
:represent
)
.
with
(
instance_of
(
Discussion
),
{
context:
instance_of
(
described_class
)
})
post
:resolve
,
request_params
end
end
end
end
end
end
end
end
...
@@ -119,6 +132,19 @@ describe Projects::DiscussionsController do
...
@@ -119,6 +132,19 @@ describe Projects::DiscussionsController do
expect
(
response
).
to
have_gitlab_http_status
(
200
)
expect
(
response
).
to
have_gitlab_http_status
(
200
)
end
end
context
"when vue_mr_discussions cookie is present"
do
before
do
allow
(
controller
).
to
receive
(
:cookies
).
and_return
({
vue_mr_discussions:
'true'
})
end
it
"renders discussion with serializer"
do
expect_any_instance_of
(
DiscussionSerializer
).
to
receive
(
:represent
)
.
with
(
instance_of
(
Discussion
),
{
context:
instance_of
(
described_class
)
})
delete
:unresolve
,
request_params
end
end
end
end
end
end
end
end
...
...
spec/controllers/projects/issues_controller_spec.rb
View file @
059ab73b
...
@@ -974,7 +974,7 @@ describe Projects::IssuesController do
...
@@ -974,7 +974,7 @@ describe Projects::IssuesController do
it
'returns discussion json'
do
it
'returns discussion json'
do
get
:discussions
,
namespace_id:
project
.
namespace
,
project_id:
project
,
id:
issue
.
iid
get
:discussions
,
namespace_id:
project
.
namespace
,
project_id:
project
,
id:
issue
.
iid
expect
(
json_response
.
first
.
keys
).
to
match_array
(
%w[id reply_id expanded notes
individual_note
]
)
expect
(
json_response
.
first
.
keys
).
to
match_array
(
%w[id reply_id expanded notes
diff_discussion individual_note resolvable resolve_with_issue_path resolved
]
)
end
end
context
'with cross-reference system note'
,
:request_store
do
context
'with cross-reference system note'
,
:request_store
do
...
...
spec/features/merge_request/user_posts_notes_spec.rb
View file @
059ab73b
...
@@ -144,7 +144,7 @@ describe 'Merge request > User posts notes', :js do
...
@@ -144,7 +144,7 @@ describe 'Merge request > User posts notes', :js do
end
end
end
end
describe
'deleting a
n attachment
'
do
describe
'deleting a
ttachment on legacy diff note
'
do
before
do
before
do
find
(
'.note'
).
hover
find
(
'.note'
).
hover
...
...
spec/fixtures/api/schemas/entities/merge_request_widget.json
View file @
059ab73b
...
@@ -75,7 +75,9 @@
...
@@ -75,7 +75,9 @@
"properties"
:
{
"properties"
:
{
"can_remove_source_branch"
:
{
"type"
:
"boolean"
},
"can_remove_source_branch"
:
{
"type"
:
"boolean"
},
"can_revert_on_current_merge_request"
:
{
"type"
:
[
"boolean"
,
"null"
]
},
"can_revert_on_current_merge_request"
:
{
"type"
:
[
"boolean"
,
"null"
]
},
"can_cherry_pick_on_current_merge_request"
:
{
"type"
:
[
"boolean"
,
"null"
]
}
"can_cherry_pick_on_current_merge_request"
:
{
"type"
:
[
"boolean"
,
"null"
]
},
"can_create_note"
:
{
"type"
:
"boolean"
},
"can_update"
:
{
"type"
:
"boolean"
}
},
},
"additionalProperties"
:
false
"additionalProperties"
:
false
},
},
...
@@ -103,6 +105,7 @@
...
@@ -103,6 +105,7 @@
"merge_ongoing"
:
{
"type"
:
"boolean"
},
"merge_ongoing"
:
{
"type"
:
"boolean"
},
"ff_only_enabled"
:
{
"type"
:
[
"boolean"
,
false
]
},
"ff_only_enabled"
:
{
"type"
:
[
"boolean"
,
false
]
},
"should_be_rebased"
:
{
"type"
:
"boolean"
},
"should_be_rebased"
:
{
"type"
:
"boolean"
},
"create_note_path"
:
{
"type"
:
[
"string"
,
"null"
]
},
"rebase_commit_sha"
:
{
"type"
:
[
"string"
,
"null"
]
},
"rebase_commit_sha"
:
{
"type"
:
[
"string"
,
"null"
]
},
"rebase_in_progress"
:
{
"type"
:
"boolean"
},
"rebase_in_progress"
:
{
"type"
:
"boolean"
},
"can_push_to_source_branch"
:
{
"type"
:
"boolean"
},
"can_push_to_source_branch"
:
{
"type"
:
"boolean"
},
...
...
spec/javascripts/autosave_spec.js
View file @
059ab73b
...
@@ -3,28 +3,24 @@ import AccessorUtilities from '~/lib/utils/accessor';
...
@@ -3,28 +3,24 @@ import AccessorUtilities from '~/lib/utils/accessor';
describe
(
'
Autosave
'
,
()
=>
{
describe
(
'
Autosave
'
,
()
=>
{
let
autosave
;
let
autosave
;
const
field
=
$
(
'
<textarea></textarea>
'
);
describe
(
'
class constructor
'
,
()
=>
{
const
key
=
'
key
'
;
const
key
=
'
key
'
;
const
field
=
jasmine
.
createSpyObj
(
'
field
'
,
[
'
data
'
,
'
on
'
]);
describe
(
'
class constructor
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
spyOn
(
AccessorUtilities
,
'
isLocalStorageAccessSafe
'
).
and
.
returnValue
(
true
);
spyOn
(
AccessorUtilities
,
'
isLocalStorageAccessSafe
'
).
and
.
returnValue
(
true
);
spyOn
(
Autosave
.
prototype
,
'
restore
'
);
spyOn
(
Autosave
.
prototype
,
'
restore
'
);
autosave
=
new
Autosave
(
field
,
key
);
});
});
it
(
'
should set .isLocalStorageAvailable
'
,
()
=>
{
it
(
'
should set .isLocalStorageAvailable
'
,
()
=>
{
autosave
=
new
Autosave
(
field
,
key
);
expect
(
AccessorUtilities
.
isLocalStorageAccessSafe
).
toHaveBeenCalled
();
expect
(
AccessorUtilities
.
isLocalStorageAccessSafe
).
toHaveBeenCalled
();
expect
(
autosave
.
isLocalStorageAvailable
).
toBe
(
true
);
expect
(
autosave
.
isLocalStorageAvailable
).
toBe
(
true
);
});
});
});
});
describe
(
'
restore
'
,
()
=>
{
describe
(
'
restore
'
,
()
=>
{
const
key
=
'
key
'
;
const
field
=
jasmine
.
createSpyObj
(
'
field
'
,
[
'
trigger
'
]);
beforeEach
(()
=>
{
beforeEach
(()
=>
{
autosave
=
{
autosave
=
{
field
,
field
,
...
@@ -49,24 +45,53 @@ describe('Autosave', () => {
...
@@ -49,24 +45,53 @@ describe('Autosave', () => {
describe
(
'
if .isLocalStorageAvailable is `true`
'
,
()
=>
{
describe
(
'
if .isLocalStorageAvailable is `true`
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
autosave
.
isLocalStorageAvailable
=
true
;
autosave
.
isLocalStorageAvailable
=
true
;
Autosave
.
prototype
.
restore
.
call
(
autosave
);
});
});
it
(
'
should call .getItem
'
,
()
=>
{
it
(
'
should call .getItem
'
,
()
=>
{
Autosave
.
prototype
.
restore
.
call
(
autosave
);
expect
(
window
.
localStorage
.
getItem
).
toHaveBeenCalledWith
(
key
);
expect
(
window
.
localStorage
.
getItem
).
toHaveBeenCalledWith
(
key
);
});
});
it
(
'
triggers jquery event
'
,
()
=>
{
spyOn
(
autosave
.
field
,
'
trigger
'
).
and
.
callThrough
();
Autosave
.
prototype
.
restore
.
call
(
autosave
);
expect
(
field
.
trigger
,
).
toHaveBeenCalled
();
});
it
(
'
triggers native event
'
,
(
done
)
=>
{
autosave
.
field
.
get
(
0
).
addEventListener
(
'
change
'
,
()
=>
{
done
();
});
Autosave
.
prototype
.
restore
.
call
(
autosave
);
});
});
});
});
describe
(
'
save
'
,
()
=>
{
describe
(
'
if field gets deleted from DOM
'
,
()
=>
{
const
field
=
jasmine
.
createSpyObj
(
'
field
'
,
[
'
val
'
]);
beforeEach
(()
=>
{
autosave
.
field
=
$
(
'
.not-a-real-element
'
);
});
it
(
'
does not trigger event
'
,
()
=>
{
spyOn
(
field
,
'
trigger
'
).
and
.
callThrough
();
expect
(
field
.
trigger
,
).
not
.
toHaveBeenCalled
();
});
});
});
describe
(
'
save
'
,
()
=>
{
beforeEach
(()
=>
{
beforeEach
(()
=>
{
autosave
=
jasmine
.
createSpyObj
(
'
autosave
'
,
[
'
reset
'
]);
autosave
=
jasmine
.
createSpyObj
(
'
autosave
'
,
[
'
reset
'
]);
autosave
.
field
=
field
;
autosave
.
field
=
field
;
field
.
val
(
'
value
'
);
field
.
val
.
and
.
returnValue
(
'
value
'
);
spyOn
(
window
.
localStorage
,
'
setItem
'
);
spyOn
(
window
.
localStorage
,
'
setItem
'
);
});
});
...
@@ -97,8 +122,6 @@ describe('Autosave', () => {
...
@@ -97,8 +122,6 @@ describe('Autosave', () => {
});
});
describe
(
'
reset
'
,
()
=>
{
describe
(
'
reset
'
,
()
=>
{
const
key
=
'
key
'
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
autosave
=
{
autosave
=
{
key
,
key
,
...
...
spec/javascripts/fixtures/merge_requests.rb
View file @
059ab73b
...
@@ -70,8 +70,50 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
...
@@ -70,8 +70,50 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_merge_request
(
example
.
description
,
merge_request
)
render_merge_request
(
example
.
description
,
merge_request
)
end
end
it
'merge_requests/discussions.json'
do
|
example
|
create
(
:diff_note_on_merge_request
,
project:
project
,
author:
admin
,
position:
position
,
noteable:
merge_request
)
render_discussions_json
(
merge_request
,
example
.
description
)
end
it
'merge_requests/diff_discussion.json'
do
|
example
|
create
(
:diff_note_on_merge_request
,
project:
project
,
author:
admin
,
position:
position
,
noteable:
merge_request
)
render_discussions_json
(
merge_request
,
example
.
description
)
end
context
'with image diff'
do
let
(
:merge_request2
)
{
create
(
:merge_request_with_diffs
,
:with_image_diffs
,
source_project:
project
,
title:
"Added images"
)
}
let
(
:image_path
)
{
"files/images/ee_repo_logo.png"
}
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
it
'merge_requests/image_diff_discussion.json'
do
|
example
|
create
(
:diff_note_on_merge_request
,
project:
project
,
noteable:
merge_request2
,
position:
image_position
)
render_discussions_json
(
merge_request2
,
example
.
description
)
end
end
private
private
def
render_discussions_json
(
merge_request
,
fixture_file_name
)
get
:discussions
,
namespace_id:
project
.
namespace
.
to_param
,
project_id:
project
,
id:
merge_request
.
to_param
,
format: :json
store_frontend_fixture
(
response
,
fixture_file_name
)
end
def
render_merge_request
(
fixture_file_name
,
merge_request
)
def
render_merge_request
(
fixture_file_name
,
merge_request
)
get
:show
,
get
:show
,
namespace_id:
project
.
namespace
.
to_param
,
namespace_id:
project
.
namespace
.
to_param
,
...
...
spec/javascripts/notes/components/comment_form_spec.js
View file @
059ab73b
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
Autosize
from
'
autosize
'
;
import
Autosize
from
'
autosize
'
;
import
store
from
'
~/notes/stores
'
;
import
store
from
'
~/notes/stores
'
;
import
issue
CommentForm
from
'
~/notes/components/comment_form.vue
'
;
import
CommentForm
from
'
~/notes/components/comment_form.vue
'
;
import
{
loggedOutnoteableData
,
notesDataMock
,
userDataMock
,
noteableDataMock
}
from
'
../mock_data
'
;
import
{
loggedOutnoteableData
,
notesDataMock
,
userDataMock
,
noteableDataMock
}
from
'
../mock_data
'
;
import
{
keyboardDownEvent
}
from
'
../../issue_show/helpers
'
;
import
{
keyboardDownEvent
}
from
'
../../issue_show/helpers
'
;
describe
(
'
issue_comment_form component
'
,
()
=>
{
describe
(
'
issue_comment_form component
'
,
()
=>
{
let
vm
;
let
vm
;
const
Component
=
Vue
.
extend
(
issue
CommentForm
);
const
Component
=
Vue
.
extend
(
CommentForm
);
let
mountComponent
;
let
mountComponent
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
mountComponent
=
()
=>
new
Component
({
mountComponent
=
(
noteableType
=
'
issue
'
)
=>
new
Component
({
propsData
:
{
noteableType
,
},
store
,
store
,
}).
$mount
();
}).
$mount
();
});
});
...
@@ -136,6 +139,11 @@ describe('issue_comment_form component', () => {
...
@@ -136,6 +139,11 @@ describe('issue_comment_form component', () => {
expect
(
vm
.
editCurrentUserLastNote
).
toHaveBeenCalled
();
expect
(
vm
.
editCurrentUserLastNote
).
toHaveBeenCalled
();
});
});
it
(
'
inits autosave
'
,
()
=>
{
expect
(
vm
.
autosave
).
toBeDefined
();
expect
(
vm
.
autosave
.
key
).
toEqual
(
`autosave/Note/Issue/
${
noteableDataMock
.
id
}
`
);
});
});
});
describe
(
'
event enter
'
,
()
=>
{
describe
(
'
event enter
'
,
()
=>
{
...
@@ -182,6 +190,15 @@ describe('issue_comment_form component', () => {
...
@@ -182,6 +190,15 @@ describe('issue_comment_form component', () => {
done
();
done
();
});
});
});
});
it
(
'
updates button text with noteable type
'
,
(
done
)
=>
{
vm
.
noteableType
=
'
merge_request
'
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.btn-comment-and-close
'
).
textContent
.
trim
()).
toEqual
(
'
Close merge request
'
);
done
();
});
});
});
});
describe
(
'
issue is confidential
'
,
()
=>
{
describe
(
'
issue is confidential
'
,
()
=>
{
...
...
spec/javascripts/notes/components/diff_file_header_spec.js
0 → 100644
View file @
059ab73b
import
Vue
from
'
vue
'
;
import
DiffFileHeader
from
'
~/notes/components/diff_file_header.vue
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
discussionFixture
=
'
merge_requests/diff_discussion.json
'
;
describe
(
'
diff_file_header
'
,
()
=>
{
let
vm
;
const
diffDiscussionMock
=
getJSONFixture
(
discussionFixture
)[
0
];
const
diffFile
=
convertObjectPropsToCamelCase
(
diffDiscussionMock
.
diff_file
);
const
props
=
{
diffFile
,
};
const
Component
=
Vue
.
extend
(
DiffFileHeader
);
const
selectors
=
{
get
copyButton
()
{
return
vm
.
$el
.
querySelector
(
'
button[data-original-title="Copy file path to clipboard"]
'
);
},
get
fileName
()
{
return
vm
.
$el
.
querySelector
(
'
.file-title-name
'
);
},
get
titleWrapper
()
{
return
vm
.
$refs
.
titleWrapper
;
},
};
describe
(
'
submodule
'
,
()
=>
{
beforeEach
(()
=>
{
props
.
diffFile
.
submodule
=
true
;
props
.
diffFile
.
submoduleLink
=
'
<a href="/bha">Submodule</a>
'
;
vm
=
mountComponent
(
Component
,
props
);
});
it
(
'
shows submoduleLink
'
,
()
=>
{
expect
(
selectors
.
fileName
.
innerHTML
).
toBe
(
props
.
diffFile
.
submoduleLink
);
});
it
(
'
has button to copy blob path
'
,
()
=>
{
expect
(
selectors
.
copyButton
).
toExist
();
expect
(
selectors
.
copyButton
.
getAttribute
(
'
data-clipboard-text
'
)).
toBe
(
props
.
diffFile
.
submoduleLink
);
});
});
describe
(
'
changed file
'
,
()
=>
{
beforeEach
(()
=>
{
props
.
diffFile
.
submodule
=
false
;
props
.
diffFile
.
discussionPath
=
'
some/discussion/id
'
;
vm
=
mountComponent
(
Component
,
props
);
});
it
(
'
shows file type icon
'
,
()
=>
{
expect
(
vm
.
$el
.
innerHTML
).
toContain
(
'
fa-file-text-o
'
);
});
it
(
'
links to discussion path
'
,
()
=>
{
expect
(
selectors
.
titleWrapper
).
toExist
();
expect
(
selectors
.
titleWrapper
.
tagName
).
toBe
(
'
A
'
);
expect
(
selectors
.
titleWrapper
.
getAttribute
(
'
href
'
)).
toBe
(
props
.
diffFile
.
discussionPath
);
});
it
(
'
shows plain title if no link given
'
,
()
=>
{
props
.
diffFile
.
discussionPath
=
undefined
;
vm
=
mountComponent
(
Component
,
props
);
expect
(
selectors
.
titleWrapper
.
tagName
).
not
.
toBe
(
'
A
'
);
expect
(
selectors
.
titleWrapper
.
href
).
toBeFalsy
();
});
it
(
'
has button to copy file path
'
,
()
=>
{
expect
(
selectors
.
copyButton
).
toExist
();
expect
(
selectors
.
copyButton
.
getAttribute
(
'
data-clipboard-text
'
)).
toBe
(
props
.
diffFile
.
filePath
);
});
it
(
'
shows file mode change
'
,
(
done
)
=>
{
vm
.
diffFile
=
{
...
props
.
diffFile
,
modeChanged
:
true
,
aMode
:
'
100755
'
,
bMode
:
'
100644
'
,
};
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$refs
.
fileMode
.
textContent
.
trim
(),
).
toBe
(
'
100755 → 100644
'
);
done
();
});
});
});
});
spec/javascripts/notes/components/diff_with_note_spec.js
0 → 100644
View file @
059ab73b
import
Vue
from
'
vue
'
;
import
DiffWithNote
from
'
~/notes/components/diff_with_note.vue
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
discussionFixture
=
'
merge_requests/diff_discussion.json
'
;
const
imageDiscussionFixture
=
'
merge_requests/image_diff_discussion.json
'
;
describe
(
'
diff_with_note
'
,
()
=>
{
let
vm
;
const
diffDiscussionMock
=
getJSONFixture
(
discussionFixture
)[
0
];
const
diffDiscussion
=
convertObjectPropsToCamelCase
(
diffDiscussionMock
);
const
Component
=
Vue
.
extend
(
DiffWithNote
);
const
props
=
{
discussion
:
diffDiscussion
,
};
const
selectors
=
{
get
container
()
{
return
vm
.
$refs
.
fileHolder
;
},
get
diffTable
()
{
return
this
.
container
.
querySelector
(
'
.diff-content table
'
);
},
get
diffRows
()
{
return
this
.
container
.
querySelectorAll
(
'
.diff-content .line_holder
'
);
},
get
noteRow
()
{
return
this
.
container
.
querySelector
(
'
.diff-content .notes_holder
'
);
},
};
describe
(
'
text diff
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
mountComponent
(
Component
,
props
);
});
it
(
'
shows text diff
'
,
()
=>
{
expect
(
selectors
.
container
).
toHaveClass
(
'
text-file
'
);
expect
(
selectors
.
diffTable
).
toExist
();
});
it
(
'
shows diff lines
'
,
()
=>
{
expect
(
selectors
.
diffRows
.
length
).
toBe
(
12
);
});
it
(
'
shows notes row
'
,
()
=>
{
expect
(
selectors
.
noteRow
).
toExist
();
});
});
describe
(
'
image diff
'
,
()
=>
{
beforeEach
(()
=>
{
const
imageDiffDiscussionMock
=
getJSONFixture
(
imageDiscussionFixture
)[
0
];
props
.
discussion
=
convertObjectPropsToCamelCase
(
imageDiffDiscussionMock
);
});
it
(
'
shows image diff
'
,
()
=>
{
vm
=
mountComponent
(
Component
,
props
);
expect
(
selectors
.
container
).
toHaveClass
(
'
js-image-file
'
);
expect
(
selectors
.
diffTable
).
not
.
toExist
();
});
});
});
spec/javascripts/notes/components/note_app_spec.js
View file @
059ab73b
...
@@ -24,6 +24,7 @@ describe('note_app', () => {
...
@@ -24,6 +24,7 @@ describe('note_app', () => {
beforeEach
(()
=>
{
beforeEach
(()
=>
{
jasmine
.
addMatchers
(
vueMatchers
);
jasmine
.
addMatchers
(
vueMatchers
);
$
(
'
body
'
).
attr
(
'
data-page
'
,
'
projects:merge_requests:show
'
);
const
IssueNotesApp
=
Vue
.
extend
(
notesApp
);
const
IssueNotesApp
=
Vue
.
extend
(
notesApp
);
...
@@ -119,8 +120,8 @@ describe('note_app', () => {
...
@@ -119,8 +120,8 @@ describe('note_app', () => {
vm
=
mountComponent
();
vm
=
mountComponent
();
});
});
it
(
'
should render loading icon
'
,
()
=>
{
it
(
'
renders skeleton notes
'
,
()
=>
{
expect
(
vm
).
toIncludeElement
(
'
.
js-loading
'
);
expect
(
vm
).
toIncludeElement
(
'
.
animation-container
'
);
});
});
it
(
'
should render form
'
,
()
=>
{
it
(
'
should render form
'
,
()
=>
{
...
...
spec/javascripts/notes/components/note_body_spec.js
View file @
059ab73b
...
@@ -30,17 +30,26 @@ describe('issue_note_body component', () => {
...
@@ -30,17 +30,26 @@ describe('issue_note_body component', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.note-text
'
).
innerHTML
).
toEqual
(
note
.
note_html
);
expect
(
vm
.
$el
.
querySelector
(
'
.note-text
'
).
innerHTML
).
toEqual
(
note
.
note_html
);
});
});
it
(
'
should be render form if user is editing
'
,
(
done
)
=>
{
it
(
'
should render awards list
'
,
()
=>
{
vm
.
isEditing
=
true
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-awards-block button [data-name="baseball"]
'
)).
not
.
toBeNull
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-awards-block button [data-name="bath_tone3"]
'
)).
not
.
toBeNull
();
});
Vue
.
nextTick
(()
=>
{
describe
(
'
isEditing
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
textarea.js-task-list-field
'
)).
toBeDefined
();
beforeEach
((
done
)
=>
{
done
();
vm
.
isEditing
=
true
;
Vue
.
nextTick
(
done
);
});
});
it
(
'
renders edit form
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
textarea.js-task-list-field
'
)).
not
.
toBeNull
();
});
});
it
(
'
should render awards list
'
,
()
=>
{
it
(
'
adds autosave
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-awards-block button [data-name="baseball"]
'
)).
toBeDefined
();
const
autosaveKey
=
`autosave/Note/
${
note
.
noteable_type
}
/
${
note
.
id
}
`
;
expect
(
vm
.
$el
.
querySelector
(
'
.js-awards-block button [data-name="bath_tone3"]
'
)).
toBeDefined
();
expect
(
vm
.
autosave
).
toExist
();
expect
(
vm
.
autosave
.
key
).
toEqual
(
autosaveKey
);
});
});
});
});
});
spec/javascripts/notes/components/note_header_spec.js
View file @
059ab73b
...
@@ -32,6 +32,7 @@ describe('note_header component', () => {
...
@@ -32,6 +32,7 @@ describe('note_header component', () => {
createdAt
:
'
2017-08-02T10:51:58.559Z
'
,
createdAt
:
'
2017-08-02T10:51:58.559Z
'
,
includeToggle
:
false
,
includeToggle
:
false
,
noteId
:
1394
,
noteId
:
1394
,
expanded
:
true
,
},
},
}).
$mount
();
}).
$mount
();
});
});
...
@@ -68,6 +69,7 @@ describe('note_header component', () => {
...
@@ -68,6 +69,7 @@ describe('note_header component', () => {
createdAt
:
'
2017-08-02T10:51:58.559Z
'
,
createdAt
:
'
2017-08-02T10:51:58.559Z
'
,
includeToggle
:
true
,
includeToggle
:
true
,
noteId
:
1395
,
noteId
:
1395
,
expanded
:
true
,
},
},
}).
$mount
();
}).
$mount
();
});
});
...
@@ -76,17 +78,35 @@ describe('note_header component', () => {
...
@@ -76,17 +78,35 @@ describe('note_header component', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button
'
)).
toBeDefined
();
});
});
it
(
'
should toggle the disucssion icon
'
,
(
done
)
=>
{
it
(
'
emits toggle event on click
'
,
(
done
)
=>
{
expect
(
spyOn
(
vm
,
'
$emit
'
);
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button i
'
).
classList
.
contains
(
'
fa-chevron-up
'
),
).
toEqual
(
true
);
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button
'
).
click
();
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button
'
).
click
();
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$emit
).
toHaveBeenCalledWith
(
'
toggleHandler
'
);
done
();
});
});
it
(
'
renders up arrow when open
'
,
(
done
)
=>
{
vm
.
expanded
=
true
;
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button i
'
).
classList
,
).
toContain
(
'
fa-chevron-up
'
);
done
();
});
});
it
(
'
renders down arrow when closed
'
,
(
done
)
=>
{
vm
.
expanded
=
false
;
Vue
.
nextTick
(()
=>
{
Vue
.
nextTick
(()
=>
{
expect
(
expect
(
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button i
'
).
classList
.
contains
(
'
fa-chevron-down
'
)
,
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button i
'
).
classList
,
).
to
Equal
(
true
);
).
to
Contain
(
'
fa-chevron-down
'
);
done
();
done
();
});
});
});
});
...
...
spec/javascripts/notes/mock_data.js
View file @
059ab73b
...
@@ -7,8 +7,9 @@ export const notesDataMock = {
...
@@ -7,8 +7,9 @@ export const notesDataMock = {
notesPath
:
'
/gitlab-org/gitlab-ce/noteable/issue/98/notes
'
,
notesPath
:
'
/gitlab-org/gitlab-ce/noteable/issue/98/notes
'
,
quickActionsDocsPath
:
'
/help/user/project/quick_actions
'
,
quickActionsDocsPath
:
'
/help/user/project/quick_actions
'
,
registerPath
:
'
/users/sign_in?redirect_to_referer=yes#register-pane
'
,
registerPath
:
'
/users/sign_in?redirect_to_referer=yes#register-pane
'
,
closeIssuePath
:
'
/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close
'
,
totalNotes
:
1
,
reopenIssuePath
:
'
/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen
'
,
closePath
:
'
/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close
'
,
reopenPath
:
'
/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen
'
,
};
};
export
const
userDataMock
=
{
export
const
userDataMock
=
{
...
...
spec/javascripts/notes/stores/getters_spec.js
View file @
059ab73b
...
@@ -56,9 +56,9 @@ describe('Getters Notes Store', () => {
...
@@ -56,9 +56,9 @@ describe('Getters Notes Store', () => {
});
});
});
});
describe
(
'
issue
State
'
,
()
=>
{
describe
(
'
open
State
'
,
()
=>
{
it
(
'
should return the issue state
'
,
()
=>
{
it
(
'
should return the issue state
'
,
()
=>
{
expect
(
getters
.
issue
State
(
state
)).
toEqual
(
noteableDataMock
.
state
);
expect
(
getters
.
open
State
(
state
)).
toEqual
(
noteableDataMock
.
state
);
});
});
});
});
});
});
spec/javascripts/notes/stores/mutation_spec.js
View file @
059ab73b
import
mutations
from
'
~/notes/stores/mutations
'
;
import
mutations
from
'
~/notes/stores/mutations
'
;
import
{
note
,
discussionMock
,
notesDataMock
,
userDataMock
,
noteableDataMock
,
individualNote
}
from
'
../mock_data
'
;
import
{
note
,
discussionMock
,
notesDataMock
,
userDataMock
,
noteableDataMock
,
individualNote
}
from
'
../mock_data
'
;
describe
(
'
Mutation Notes Store
'
,
()
=>
{
describe
(
'
Notes Store mutations
'
,
()
=>
{
describe
(
'
ADD_NEW_NOTE
'
,
()
=>
{
describe
(
'
ADD_NEW_NOTE
'
,
()
=>
{
let
state
;
let
state
;
let
noteData
;
let
noteData
;
...
@@ -103,7 +103,8 @@ describe('Mutation Notes Store', () => {
...
@@ -103,7 +103,8 @@ describe('Mutation Notes Store', () => {
};
};
mutations
.
SET_INITIAL_NOTES
(
state
,
[
note
]);
mutations
.
SET_INITIAL_NOTES
(
state
,
[
note
]);
expect
(
state
.
notes
).
toEqual
([
note
]);
expect
(
state
.
notes
[
0
].
id
).
toEqual
(
note
.
id
);
expect
(
state
.
notes
.
length
).
toEqual
(
1
);
});
});
});
});
...
...
spec/javascripts/notes_spec.js
View file @
059ab73b
...
@@ -34,6 +34,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
...
@@ -34,6 +34,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe
(
'
Notes
'
,
function
()
{
describe
(
'
Notes
'
,
function
()
{
const
FLASH_TYPE_ALERT
=
'
alert
'
;
const
FLASH_TYPE_ALERT
=
'
alert
'
;
const
NOTES_POST_PATH
=
/
(
.*
)\/
notes
\?
html=true$/
;
var
commentsTemplate
=
'
merge_requests/merge_request_with_comment.html.raw
'
;
var
commentsTemplate
=
'
merge_requests/merge_request_with_comment.html.raw
'
;
preloadFixtures
(
commentsTemplate
);
preloadFixtures
(
commentsTemplate
);
...
@@ -154,7 +155,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
...
@@ -154,7 +155,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$form
.
find
(
'
textarea.js-note-text
'
).
val
(
sampleComment
);
$form
.
find
(
'
textarea.js-note-text
'
).
val
(
sampleComment
);
mock
=
new
MockAdapter
(
axios
);
mock
=
new
MockAdapter
(
axios
);
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
reply
(
200
,
noteEntity
);
mock
.
onPost
(
NOTES_POST_PATH
).
reply
(
200
,
noteEntity
);
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -506,11 +507,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
...
@@ -506,11 +507,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
let
mock
;
let
mock
;
function
mockNotesPost
()
{
function
mockNotesPost
()
{
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
reply
(
200
,
note
);
mock
.
onPost
(
NOTES_POST_PATH
).
reply
(
200
,
note
);
}
}
function
mockNotesPostError
()
{
function
mockNotesPostError
()
{
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
networkError
();
mock
.
onPost
(
NOTES_POST_PATH
).
networkError
();
}
}
beforeEach
(()
=>
{
beforeEach
(()
=>
{
...
@@ -631,7 +632,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
...
@@ -631,7 +632,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
beforeEach
(()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
=
new
MockAdapter
(
axios
);
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
reply
(
200
,
note
);
mock
.
onPost
(
NOTES_POST_PATH
).
reply
(
200
,
note
);
this
.
notes
=
new
Notes
(
''
,
[]);
this
.
notes
=
new
Notes
(
''
,
[]);
window
.
gon
.
current_username
=
'
root
'
;
window
.
gon
.
current_username
=
'
root
'
;
...
@@ -684,7 +685,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
...
@@ -684,7 +685,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
beforeEach
(()
=>
{
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
=
new
MockAdapter
(
axios
);
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
reply
(
200
,
note
);
mock
.
onPost
(
NOTES_POST_PATH
).
reply
(
200
,
note
);
this
.
notes
=
new
Notes
(
''
,
[]);
this
.
notes
=
new
Notes
(
''
,
[]);
window
.
gon
.
current_username
=
'
root
'
;
window
.
gon
.
current_username
=
'
root
'
;
...
...
spec/serializers/diff_file_entity_spec.rb
0 → 100644
View file @
059ab73b
require
'spec_helper'
describe
DiffFileEntity
do
include
RepoHelpers
let
(
:project
)
{
create
(
:project
,
:repository
)
}
let
(
:repository
)
{
project
.
repository
}
let
(
:commit
)
{
project
.
commit
(
sample_commit
.
id
)
}
let
(
:diff_refs
)
{
commit
.
diff_refs
}
let
(
:diff
)
{
commit
.
raw_diffs
.
first
}
let
(
:diff_file
)
{
Gitlab
::
Diff
::
File
.
new
(
diff
,
diff_refs:
diff_refs
,
repository:
repository
)
}
let
(
:entity
)
{
described_class
.
new
(
diff_file
)
}
subject
{
entity
.
as_json
}
it
'exposes correct attributes'
do
expect
(
subject
).
to
include
(
:submodule
,
:submodule_link
,
:file_path
,
:deleted_file
,
:old_path
,
:new_path
,
:mode_changed
,
:a_mode
,
:b_mode
,
:text
,
:old_path_html
,
:new_path_html
)
end
end
spec/serializers/discussion_entity_spec.rb
0 → 100644
View file @
059ab73b
require
'spec_helper'
describe
DiscussionEntity
do
include
RepoHelpers
let
(
:user
)
{
create
(
:user
)
}
let
(
:note
)
{
create
(
:discussion_note_on_merge_request
)
}
let
(
:discussion
)
{
note
.
discussion
}
let
(
:request
)
{
double
(
'request'
)
}
let
(
:controller
)
{
double
(
'controller'
)
}
let
(
:entity
)
{
described_class
.
new
(
discussion
,
request:
request
,
context:
controller
)
}
subject
{
entity
.
as_json
}
before
do
allow
(
controller
).
to
receive
(
:render_to_string
)
allow
(
request
).
to
receive
(
:current_user
).
and_return
(
user
)
allow
(
request
).
to
receive
(
:noteable
).
and_return
(
note
.
noteable
)
end
it
'exposes correct attributes'
do
expect
(
subject
).
to
include
(
:id
,
:expanded
,
:notes
,
:individual_note
,
:resolvable
,
:resolved
,
:resolve_path
,
:resolve_with_issue_path
,
:diff_discussion
)
end
context
'when diff file is present'
do
let
(
:note
)
{
create
(
:diff_note_on_merge_request
)
}
it
'exposes diff file attributes'
do
expect
(
subject
).
to
include
(
:diff_file
,
:truncated_diff_lines
,
:image_diff_html
)
end
end
end
spec/serializers/note_entity_spec.rb
View file @
059ab73b
...
@@ -48,4 +48,15 @@ describe NoteEntity do
...
@@ -48,4 +48,15 @@ describe NoteEntity do
expect
(
subject
).
to
include
(
:system_note_icon_name
)
expect
(
subject
).
to
include
(
:system_note_icon_name
)
end
end
end
end
context
'when note is part of resolvable discussion'
do
before
do
allow
(
note
).
to
receive
(
:part_of_discussion?
).
and_return
(
true
)
allow
(
note
).
to
receive
(
:resolvable?
).
and_return
(
true
)
end
it
'exposes paths to resolve note'
do
expect
(
subject
).
to
include
(
:resolve_path
,
:resolve_with_issue_path
)
end
end
end
end
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