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
1
Merge Requests
1
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
nexedi
gitlab-ce
Commits
85ff85f1
Commit
85ff85f1
authored
Feb 28, 2018
by
Jacob Schatz
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'ee-acet-mr-notes-index' into 'master'
MR Notes Refactor See merge request gitlab-org/gitlab-ee!4245
parents
8660f732
6d3648af
Changes
74
Hide whitespace changes
Inline
Side-by-side
Showing
74 changed files
with
1746 additions
and
326 deletions
+1746
-326
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
+83
-53
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 @
85ff85f1
...
...
@@ -3,10 +3,10 @@
import
AccessorUtilities
from
'
./lib/utils/accessor
'
;
export
default
class
Autosave
{
constructor
(
field
,
key
,
resource
)
{
constructor
(
field
,
key
)
{
this
.
field
=
field
;
this
.
isLocalStorageAvailable
=
AccessorUtilities
.
isLocalStorageAccessSafe
();
this
.
resource
=
resource
;
if
(
key
.
join
!=
null
)
{
key
=
key
.
join
(
'
/
'
);
}
...
...
@@ -17,31 +17,27 @@ export default class Autosave {
}
restore
()
{
var
text
;
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
)
{
this
.
field
.
val
(
text
);
}
if
(
!
this
.
resource
&&
this
.
resource
!==
'
issue
'
)
{
this
.
field
.
trigger
(
'
input
'
);
}
else
{
// v-model does not update with jQuery trigger
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const
event
=
new
Event
(
'
change
'
,
{
bubbles
:
true
,
cancelable
:
false
});
const
field
=
this
.
field
.
get
(
0
);
if
(
field
)
{
field
.
dispatchEvent
(
event
);
}
}
this
.
field
.
trigger
(
'
input
'
);
// v-model does not update with jQuery trigger
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const
event
=
new
Event
(
'
change
'
,
{
bubbles
:
true
,
cancelable
:
false
});
const
field
=
this
.
field
.
get
(
0
);
field
.
dispatchEvent
(
event
);
}
save
()
{
var
text
;
text
=
this
.
field
.
val
();
if
(
!
this
.
field
.
length
)
return
;
const
text
=
this
.
field
.
val
();
if
(
this
.
isLocalStorageAvailable
&&
(
text
!=
null
?
text
.
length
:
void
0
)
>
0
)
{
return
window
.
localStorage
.
setItem
(
this
.
key
,
text
);
...
...
app/assets/javascripts/awards_handler.js
View file @
85ff85f1
...
...
@@ -2,7 +2,7 @@
import
_
from
'
underscore
'
;
import
Cookies
from
'
js-cookie
'
;
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
axios
from
'
./lib/utils/axios_utils
'
;
...
...
@@ -239,9 +239,9 @@ class AwardsHandler {
}
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_
'
,
''
);
this
.
hideMenuElement
(
$
(
'
.emoji-menu
'
));
...
...
@@ -293,8 +293,16 @@ class AwardsHandler {
}
}
isVueMRDiscussions
()
{
return
isInMRPage
()
&&
hasVueMRDiscussionsCookie
()
&&
!
$
(
'
#diffs
'
).
is
(
'
:visible
'
);
}
isInVueNoteablePage
()
{
return
isInIssuePage
()
||
this
.
isVueMRDiscussions
();
}
getVotesBlock
()
{
if
(
isInIssu
ePage
())
{
if
(
this
.
isInVueNoteabl
ePage
())
{
const
$el
=
$
(
'
.js-add-award.is-active
'
).
closest
(
'
.note.timeline-entry
'
);
if
(
$el
.
length
)
{
...
...
app/assets/javascripts/diff_notes/components/jump_to_discussion.js
View file @
85ff85f1
...
...
@@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({
}
$
.
scrollTo
(
$target
,
{
offset
:
0
offset
:
-
15
0
});
}
},
...
...
app/assets/javascripts/diff_notes/components/resolve_btn.js
View file @
85ff85f1
...
...
@@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({
CommentsStore
.
update
(
this
.
discussionId
,
this
.
noteId
,
!
this
.
isResolved
,
resolved_by
);
this
.
discussion
.
updateHeadline
(
data
);
gl
.
mrWidget
.
checkStatus
();
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshVueNotes
'
));
this
.
updateTooltip
();
})
...
...
app/assets/javascripts/diff_notes/diff_notes_bundle.js
View file @
85ff85f1
...
...
@@ -14,6 +14,7 @@ import './components/resolve_count';
import
'
./components/resolve_discussion_btn
'
;
import
'
./components/diff_note_avatars
'
;
import
'
./components/new_issue_for_discussion
'
;
import
{
hasVueMRDiscussionsCookie
}
from
'
../lib/utils/common_utils
'
;
export
default
()
=>
{
const
projectPathHolder
=
document
.
querySelector
(
'
.merge-request
'
)
||
document
.
querySelector
(
'
.commit-box
'
);
...
...
@@ -67,12 +68,14 @@ export default () => {
gl
.
diffNotesCompileComponents
();
new
Vue
({
el
:
'
#resolve-count-app
'
,
components
:
{
'
resolve-count
'
:
ResolveCount
},
});
if
(
!
hasVueMRDiscussionsCookie
())
{
new
Vue
({
el
:
'
#resolve-count-app
'
,
components
:
{
'
resolve-count
'
:
ResolveCount
}
});
}
$
(
window
).
trigger
(
'
resize.nav
'
);
};
app/assets/javascripts/diff_notes/services/resolve.js
View file @
85ff85f1
...
...
@@ -8,8 +8,8 @@ window.gl = window.gl || {};
class
ResolveServiceClass
{
constructor
(
root
)
{
this
.
noteResource
=
Vue
.
resource
(
`
${
root
}
/notes{/noteId}/resolve`
);
this
.
discussionResource
=
Vue
.
resource
(
`
${
root
}
/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`
);
this
.
noteResource
=
Vue
.
resource
(
`
${
root
}
/notes{/noteId}/resolve
?html=true
`
);
this
.
discussionResource
=
Vue
.
resource
(
`
${
root
}
/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve
?html=true
`
);
}
resolve
(
noteId
)
{
...
...
@@ -45,6 +45,7 @@ class ResolveServiceClass {
if
(
gl
.
mrWidget
)
gl
.
mrWidget
.
checkStatus
();
discussion
.
updateHeadline
(
data
);
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshVueNotes
'
));
})
.
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 @
85ff85f1
import
jQuery
from
'
jquery
'
;
import
Cookies
from
'
js-cookie
'
;
import
axios
from
'
./axios_utils
'
;
import
{
getLocationHash
}
from
'
./url_utility
'
;
import
{
convertToCamelCase
}
from
'
./text_utility
'
;
...
...
@@ -22,13 +24,18 @@ export const getGroupSlug = () => {
return
null
;
};
export
const
isInIssuePage
=
(
)
=>
{
const
page
=
getPagePath
(
1
);
const
action
=
getPagePath
(
2
);
export
const
checkPageAndAction
=
(
page
,
action
)
=>
{
const
page
Path
=
getPagePath
(
1
);
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
,
{
params
:
{
format
:
'
js
'
},
responseType
:
'
text
'
,
...
...
@@ -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)
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
mrTabsHeight
=
$
(
'
.merge-request-tabs
'
).
height
()
||
0
;
const
headerHeight
=
$
(
'
.navbar-gitlab
'
).
height
()
||
0
;
...
...
app/assets/javascripts/lib/utils/text_utility.js
View file @
85ff85f1
...
...
@@ -65,6 +65,20 @@ export function capitalizeFirstCharacter(text) {
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.
*
...
...
app/assets/javascripts/merge_request_tabs.js
View file @
85ff85f1
...
...
@@ -241,6 +241,10 @@ export default class MergeRequestTabs {
return
newState
;
}
getCurrentAction
()
{
return
this
.
currentAction
;
}
loadCommits
(
source
)
{
if
(
this
.
commitsLoaded
)
{
return
;
...
...
app/assets/javascripts/mr_notes/index.js
0 → 100644
View file @
85ff85f1
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 @
85ff85f1
...
...
@@ -24,7 +24,7 @@ import GLForm from './gl_form';
import
loadAwardsHandler
from
'
./awards_handler
'
;
import
Autosave
from
'
./autosave
'
;
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
{
localTimeAgo
}
from
'
./lib/utils/datetime_utility
'
;
...
...
@@ -44,6 +44,10 @@ export default class Notes {
}
}
static
getInstance
()
{
return
this
.
instance
;
}
constructor
(
notes_url
,
note_ids
,
last_fetched_at
,
view
,
enableGFM
=
true
)
{
this
.
updateTargetButtons
=
this
.
updateTargetButtons
.
bind
(
this
);
this
.
updateComment
=
this
.
updateComment
.
bind
(
this
);
...
...
@@ -102,67 +106,77 @@ export default class Notes {
}
addBinding
()
{
this
.
$wrapperEl
=
hasVueMRDiscussionsCookie
()
?
$
(
document
).
find
(
'
.diffs
'
)
:
$
(
document
);
// Edit note link
$
(
document
)
.
on
(
'
click
'
,
'
.js-note-edit
'
,
this
.
showEditForm
.
bind
(
this
));
$
(
document
)
.
on
(
'
click
'
,
'
.note-edit-cancel
'
,
this
.
cancelEdit
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-edit
'
,
this
.
showEditForm
.
bind
(
this
));
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.note-edit-cancel
'
,
this
.
cancelEdit
);
// Reopen and close actions for Issue/MR combined with note form submit
$
(
document
)
.
on
(
'
click
'
,
'
.js-comment-submit-button
'
,
this
.
postComment
);
$
(
document
)
.
on
(
'
click
'
,
'
.js-comment-save-button
'
,
this
.
updateComment
);
$
(
document
)
.
on
(
'
keyup input
'
,
'
.js-note-text
'
,
this
.
updateTargetButtons
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-submit-button
'
,
this
.
postComment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-save-button
'
,
this
.
updateComment
);
this
.
$wrapperEl
.
on
(
'
keyup input
'
,
'
.js-note-text
'
,
this
.
updateTargetButtons
);
// 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)
$
(
document
)
.
on
(
'
click
'
,
'
.js-note-delete
'
,
this
.
removeNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-delete
'
,
this
.
removeNote
);
// 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
$
(
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
$
(
document
)
.
on
(
'
change
'
,
'
.js-note-attachment-input
'
,
this
.
updateFormAttachment
);
this
.
$wrapperEl
.
on
(
'
change
'
,
'
.js-note-attachment-input
'
,
this
.
updateFormAttachment
);
// 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
$
(
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
$
(
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
$
(
document
)
.
on
(
'
click
'
,
'
.js-close-discussion-note-form
'
,
this
.
cancelDiscussionForm
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-close-discussion-note-form
'
,
this
.
cancelDiscussionForm
);
// 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
$
(
document
)
.
on
(
'
visibilitychange
'
,
this
.
visibilityChange
);
this
.
$wrapperEl
.
on
(
'
visibilitychange
'
,
this
.
visibilityChange
);
// 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.
$
(
document
)
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
addNote
);
$
(
document
)
.
on
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
,
this
.
addDiscussionNote
);
$
(
document
)
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
resetMainTargetForm
);
$
(
document
)
.
on
(
'
ajax:complete
'
,
'
.js-main-target-form
'
,
this
.
reenableTargetFormSubmitButton
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
addNote
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
,
this
.
addDiscussionNote
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
resetMainTargetForm
);
this
.
$wrapperEl
.
on
(
'
ajax:complete
'
,
'
.js-main-target-form
'
,
this
.
reenableTargetFormSubmitButton
);
// 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`
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
()
{
$
(
document
).
off
(
'
click
'
,
'
.js-note-edit
'
);
$
(
document
).
off
(
'
click
'
,
'
.note-edit-cancel
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-delete
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-attachment-delete
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-discussion-reply-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-add-diff-note-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-add-image-diff-note-button
'
);
$
(
document
).
off
(
'
visibilitychange
'
);
$
(
document
).
off
(
'
keyup input
'
,
'
.js-note-text
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-target-reopen
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-target-close
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-note-discard
'
);
$
(
document
).
off
(
'
keydown
'
,
'
.js-note-text
'
);
$
(
document
).
off
(
'
click
'
,
'
.js-comment-resolve-button
'
);
$
(
document
).
off
(
'
click
'
,
'
.system-note-commit-list-toggler
'
);
$
(
document
).
off
(
'
ajax:success
'
,
'
.js-main-target-form
'
);
$
(
document
).
off
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
);
$
(
document
).
off
(
'
ajax:complete
'
,
'
.js-main-target-form
'
);
if
(
!
this
.
eventsBound
)
{
return
;
}
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-edit
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.note-edit-cancel
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-delete
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-attachment-delete
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-discussion-reply-button
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-add-diff-note-button
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-add-image-diff-note-button
'
);
this
.
$wrapperEl
.
off
(
'
visibilitychange
'
);
this
.
$wrapperEl
.
off
(
'
keyup input
'
,
'
.js-note-text
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-target-reopen
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-target-close
'
);
this
.
$wrapperEl
.
off
(
'
click
'
,
'
.js-note-discard
'
);
this
.
$wrapperEl
.
off
(
'
keydown
'
,
'
.js-note-text
'
);
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
);
}
...
...
@@ -252,8 +266,10 @@ export default class Notes {
if
(
this
.
refreshing
)
{
return
;
}
this
.
refreshing
=
true
;
axios
.
get
(
this
.
notes_url
,
{
axios
.
get
(
`
${
this
.
notes_url
}
?html=true`
,
{
headers
:
{
'
X-Last-Fetched-At
'
:
this
.
last_fetched_at
,
},
...
...
@@ -350,7 +366,7 @@ export default class Notes {
}
if
(
!
noteEntity
.
valid
)
{
if
(
noteEntity
.
errors
.
commands_only
)
{
if
(
noteEntity
.
errors
&&
noteEntity
.
errors
.
commands_only
)
{
if
(
noteEntity
.
commands_changes
&&
Object
.
keys
(
noteEntity
.
commands_changes
).
length
>
0
)
{
$notesList
.
find
(
'
.system-note.being-posted
'
).
remove
();
...
...
@@ -363,6 +379,10 @@ export default class Notes {
const
$note
=
$notesList
.
find
(
`#note_
${
noteEntity
.
id
}
`
);
if
(
Notes
.
isNewNote
(
noteEntity
,
this
.
note_ids
))
{
if
(
hasVueMRDiscussionsCookie
())
{
return
;
}
this
.
note_ids
.
push
(
noteEntity
.
id
);
if
(
$notesList
.
length
)
{
...
...
@@ -399,6 +419,8 @@ export default class Notes {
this
.
setupNewNote
(
$updatedNote
);
}
}
Notes
.
refreshVueNotes
();
}
isParallelView
()
{
...
...
@@ -406,12 +428,11 @@ export default class Notes {
}
/**
* Render note in discussion area.
*
* Note: for rendering inline notes use renderDiscussionNote
* Render note in discussion area. To render inline notes use renderDiscussionNote.
*/
renderDiscussionNote
(
noteEntity
,
$form
)
{
var
discussionContainer
,
form
,
row
,
lineType
,
diffAvatarContainer
;
if
(
!
Notes
.
isNewNote
(
noteEntity
,
this
.
note_ids
))
{
return
;
}
...
...
@@ -452,7 +473,9 @@ export default class Notes {
// Init discussion on 'Discussion' page if it is merge request page
const
page
=
$
(
'
body
'
).
attr
(
'
data-page
'
);
if
((
page
&&
page
.
indexOf
(
'
projects:merge_request
'
)
!==
-
1
)
||
!
noteEntity
.
diff_discussion_html
)
{
Notes
.
animateAppendNote
(
noteEntity
.
discussion_html
,
$
(
'
.main-notes-list
'
));
if
(
!
hasVueMRDiscussionsCookie
())
{
Notes
.
animateAppendNote
(
noteEntity
.
discussion_html
,
$
(
'
.main-notes-list
'
));
}
}
}
else
{
// append new note to all matching discussions
...
...
@@ -634,7 +657,6 @@ export default class Notes {
var
$noteEntityEl
,
$note_li
;
// Convert returned HTML to a jQuery object so we can modify it further
$noteEntityEl
=
$
(
noteEntity
.
html
);
$noteEntityEl
.
addClass
(
'
fade-in-full
'
);
this
.
revertNoteEditForm
(
$targetNote
);
$noteEntityEl
.
renderGFM
();
// Find the note's `li` element by ID and replace it with the updated HTML
...
...
@@ -730,7 +752,7 @@ export default class Notes {
var
selector
=
this
.
getEditFormSelector
(
$target
);
var
$editForm
=
$
(
selector
);
$editForm
.
insertBefore
(
'
.
notes-form
'
);
$editForm
.
insertBefore
(
'
.
diffs
'
);
$editForm
.
find
(
'
.js-comment-save-button
'
).
enable
();
$editForm
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
}
...
...
@@ -747,6 +769,7 @@ export default class Notes {
removeNoteEditForm
(
$note
)
{
var
form
=
$note
.
find
(
'
.current-note-edit-form
'
);
$note
.
removeClass
(
'
is-editing
'
);
form
.
removeClass
(
'
current-note-edit-form
'
);
form
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
...
...
@@ -818,6 +841,7 @@ export default class Notes {
};
})(
this
));
Notes
.
refreshVueNotes
();
Notes
.
checkMergeRequestStatus
();
return
this
.
updateNotesCount
(
-
1
);
}
...
...
@@ -1157,7 +1181,7 @@ export default class Notes {
this
.
glForm
=
new
GLForm
(
$editForm
.
find
(
'
form
'
),
this
.
enableGFM
);
$editForm
.
find
(
'
form
'
)
.
attr
(
'
action
'
,
postUrl
)
.
attr
(
'
action
'
,
`
${
postUrl
}
?html=true`
)
.
attr
(
'
data-remote
'
,
'
true
'
);
$editForm
.
find
(
'
.js-form-target-id
'
).
val
(
targetId
);
$editForm
.
find
(
'
.js-form-target-type
'
).
val
(
targetType
);
...
...
@@ -1280,6 +1304,10 @@ export default class Notes {
return
$updatedNote
;
}
static
refreshVueNotes
()
{
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshVueNotes
'
));
}
/**
* Get data from Form attributes to use for saving/submitting comment.
*/
...
...
@@ -1481,7 +1509,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
axios
.
post
(
formAction
,
formData
)
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
((
res
)
=>
{
const
note
=
res
.
data
;
...
...
@@ -1546,6 +1574,8 @@ export default class Notes {
if
(
$notesContainer
.
length
)
{
$notesContainer
.
append
(
'
<div class="flash-container" style="display: none;"></div>
'
);
}
Notes
.
refreshVueNotes
();
}
else
if
(
isMainForm
)
{
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
this
.
addNote
(
$form
,
note
);
...
...
@@ -1627,7 +1657,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
axios
.
post
(
formAction
,
formData
)
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
(({
data
})
=>
{
// Submission successful! render final note element
this
.
updateNote
(
data
,
$editingNote
);
...
...
app/assets/javascripts/notes/components/comment_form.vue
View file @
85ff85f1
...
...
@@ -2,10 +2,11 @@
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
_
from
'
underscore
'
;
import
Autosize
from
'
autosize
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
Flash
from
'
../../flash
'
;
import
Autosave
from
'
../../autosave
'
;
import
TaskList
from
'
../../task_list
'
;
import
{
capitalizeFirstCharacter
,
convertToCamelCase
}
from
'
../../lib/utils/text_utility
'
;
import
*
as
constants
from
'
../constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
...
...
@@ -29,6 +30,12 @@
mixins
:
[
issuableStateMixin
,
],
props
:
{
noteableType
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
note
:
''
,
...
...
@@ -43,37 +50,51 @@
'
getUserData
'
,
'
getNoteableData
'
,
'
getNotesData
'
,
'
issue
State
'
,
'
open
State
'
,
]),
noteableDisplayName
()
{
return
this
.
noteableType
.
replace
(
/_/g
,
'
'
);
},
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
commentButtonTitle
()
{
return
this
.
noteType
===
constants
.
COMMENT
?
'
Comment
'
:
'
Start discussion
'
;
},
is
Issue
Open
()
{
return
this
.
issueState
===
constants
.
OPENED
||
this
.
issue
State
===
constants
.
REOPENED
;
isOpen
()
{
return
this
.
openState
===
constants
.
OPENED
||
this
.
open
State
===
constants
.
REOPENED
;
},
canCreateNote
()
{
return
this
.
getNoteableData
.
current_user
.
can_create_note
;
},
issueActionButtonTitle
()
{
if
(
this
.
note
.
length
)
{
const
actionText
=
this
.
isIssueOpen
?
'
close
'
:
'
reopen
'
;
const
openOrClose
=
this
.
isOpen
?
'
close
'
:
'
reopen
'
;
return
this
.
noteType
===
constants
.
COMMENT
?
`Comment &
${
actionText
}
issue`
:
`Start discussion &
${
actionText
}
issue`
;
if
(
this
.
note
.
length
)
{
return
sprintf
(
__
(
'
%{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
()
{
return
{
'
btn-reopen
'
:
!
this
.
is
Issue
Open
,
'
btn-close
'
:
this
.
is
Issue
Open
,
'
js-note-target-close
'
:
this
.
is
Issue
Open
,
'
js-note-target-reopen
'
:
!
this
.
is
Issue
Open
,
'
btn-reopen
'
:
!
this
.
isOpen
,
'
btn-close
'
:
this
.
isOpen
,
'
js-note-target-close
'
:
this
.
isOpen
,
'
js-note-target-reopen
'
:
!
this
.
isOpen
,
};
},
markdownDocsPath
()
{
...
...
@@ -138,7 +159,7 @@
flashContainer
:
this
.
$el
,
data
:
{
note
:
{
noteable_type
:
constants
.
NOTEABLE_TYPE
,
noteable_type
:
this
.
noteableType
,
noteable_id
:
this
.
getNoteableData
.
id
,
note
:
this
.
note
,
},
...
...
@@ -193,19 +214,29 @@ Please check your network connection and try again.`;
this
.
isSubmitting
=
false
;
},
toggleIssueState
()
{
if
(
this
.
is
Issue
Open
)
{
if
(
this
.
isOpen
)
{
this
.
closeIssue
()
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
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
{
this
.
reopenIssue
()
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
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.`;
this
.
$refs
.
markdownField
.
previewMarkdown
=
false
;
}
// reset autostave
this
.
autosave
.
reset
();
},
setNoteType
(
type
)
{
...
...
@@ -240,10 +270,11 @@ Please check your network connection and try again.`;
},
initAutoSave
()
{
if
(
this
.
isLoggedIn
)
{
const
noteableType
=
capitalizeFirstCharacter
(
convertToCamelCase
(
this
.
noteableType
));
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
textarea
),
[
'
Note
'
,
'
Issue
'
,
this
.
getNoteableData
.
id
],
'
issue
'
,
[
'
Note
'
,
noteableType
,
this
.
getNoteableData
.
id
],
);
}
},
...
...
@@ -331,7 +362,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled=
"isSubmitButtonDisabled"
class=
"btn btn-create comment-btn js-comment-button js-comment-submit-button"
type=
"submit"
>
{{
commentButtonTitle
}}
{{
__
(
commentButtonTitle
)
}}
</button>
<button
:disabled=
"isSubmitButtonDisabled"
...
...
@@ -359,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<div
class=
"description"
>
<strong>
Comment
</strong>
<p>
Add a general comment to this
issue
.
Add a general comment to this
{{
noteableDisplayName
}}
.
</p>
</div>
</button>
...
...
app/assets/javascripts/notes/components/diff_file_header.vue
0 → 100644
View file @
85ff85f1
<
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 @
85ff85f1
<
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 @
85ff85f1
<
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 @
85ff85f1
...
...
@@ -4,6 +4,8 @@
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.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
loadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
...
...
@@ -42,6 +44,26 @@
type
:
Boolean
,
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
:
{
type
:
Boolean
,
required
:
true
,
...
...
@@ -63,6 +85,15 @@
currentUserId
()
{
return
this
.
getUserDataByProp
(
'
id
'
);
},
resolveButtonTitle
()
{
let
title
=
'
Mark as resolved
'
;
if
(
this
.
resolvedBy
)
{
title
=
`Resolved by
${
this
.
resolvedBy
.
name
}
`
;
}
return
title
;
},
},
created
()
{
this
.
emojiSmiling
=
emojiSmiling
;
...
...
@@ -70,6 +101,8 @@
this
.
emojiSmiley
=
emojiSmiley
;
this
.
editSvg
=
editSvg
;
this
.
ellipsisSvg
=
ellipsisSvg
;
this
.
resolveDiscussionSvg
=
resolveDiscussionSvg
;
this
.
resolvedDiscussionSvg
=
resolvedDiscussionSvg
;
},
methods
:
{
onEdit
()
{
...
...
@@ -78,6 +111,9 @@
onDelete
()
{
this
.
$emit
(
'
handleDelete
'
);
},
onResolve
()
{
this
.
$emit
(
'
handleResolve
'
);
},
},
};
</
script
>
...
...
@@ -89,6 +125,31 @@
class=
"note-role user-access-role"
>
{{
accessLevel
}}
</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
v-if=
"canAddAwardEmoji"
class=
"note-actions-item"
>
...
...
app/assets/javascripts/notes/components/note_body.vue
View file @
85ff85f1
...
...
@@ -41,7 +41,7 @@
this
.
initTaskList
();
if
(
this
.
isEditing
)
{
this
.
initAutoSave
();
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
}
},
updated
()
{
...
...
@@ -50,7 +50,7 @@
if
(
this
.
isEditing
)
{
if
(
!
this
.
autosave
)
{
this
.
initAutoSave
();
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
}
else
{
this
.
setAutoSave
();
}
...
...
app/assets/javascripts/notes/components/note_form.vue
View file @
85ff85f1
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
export
default
{
name
:
'
IssueNoteForm
'
,
...
...
@@ -13,6 +14,7 @@
},
mixins
:
[
issuableStateMixin
,
resolvable
,
],
props
:
{
noteBody
:
{
...
...
@@ -30,7 +32,7 @@
required
:
false
,
default
:
'
Save comment
'
,
},
discussion
:
{
note
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
...
...
@@ -42,9 +44,11 @@
},
data
()
{
return
{
note
:
this
.
noteBody
,
updatedNoteBody
:
this
.
noteBody
,
conflictWhileEditing
:
false
,
isSubmitting
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
};
},
computed
:
{
...
...
@@ -71,13 +75,13 @@
return
this
.
getUserDataByProp
(
'
id
'
);
},
isDisabled
()
{
return
!
this
.
note
.
length
||
this
.
isSubmitting
;
return
!
this
.
updatedNoteBody
.
length
||
this
.
isSubmitting
;
},
},
watch
:
{
noteBody
()
{
if
(
this
.
note
===
this
.
noteBody
)
{
this
.
note
=
this
.
noteBody
;
if
(
this
.
updatedNoteBody
===
this
.
noteBody
)
{
this
.
updatedNoteBody
=
this
.
noteBody
;
}
else
{
this
.
conflictWhileEditing
=
true
;
}
...
...
@@ -87,16 +91,24 @@
this
.
$refs
.
textarea
.
focus
();
},
methods
:
{
handleUpdate
()
{
...
mapActions
([
'
toggleResolveNote
'
,
]),
handleUpdate
(
shouldResolve
)
{
const
beforeSubmitDiscussionState
=
this
.
discussionResolved
;
this
.
isSubmitting
=
true
;
this
.
$emit
(
'
handleFormUpdate
'
,
this
.
note
,
this
.
$refs
.
editNoteForm
,
()
=>
{
this
.
$emit
(
'
handleFormUpdate
'
,
this
.
updatedNoteBody
,
this
.
$refs
.
editNoteForm
,
()
=>
{
this
.
isSubmitting
=
false
;
if
(
shouldResolve
)
{
this
.
resolveHandler
(
beforeSubmitDiscussionState
);
}
});
},
editMyLastNote
()
{
if
(
this
.
note
===
''
)
{
const
lastNoteInDiscussion
=
this
.
getDiscussionLastNote
(
this
.
discussion
);
if
(
this
.
updatedNoteBody
===
''
)
{
const
lastNoteInDiscussion
=
this
.
getDiscussionLastNote
(
this
.
updatedNoteBody
);
if
(
lastNoteInDiscussion
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
...
...
@@ -107,7 +119,7 @@
},
cancelHandler
(
shouldConfirm
=
false
)
{
// 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 @@
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
:data-supports-quick-actions=
"!isEditing"
aria-label=
"Description"
v-model=
"
note
"
v-model=
"
updatedNoteBody
"
ref=
"textarea"
slot=
"textarea"
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"
class=
"js-vue-issue-save btn btn-save"
>
{{
saveButtonTitle
}}
</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
@
click=
"cancelHandler()"
class=
"btn btn-cancel note-edit-cancel"
...
...
app/assets/javascripts/notes/components/note_header.vue
View file @
85ff85f1
...
...
@@ -34,15 +34,15 @@
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
isExpanded
:
true
,
}
;
expanded
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
}
,
},
computed
:
{
toggleChevronClass
()
{
return
this
.
isE
xpanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
return
this
.
e
xpanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
},
noteTimestampLink
()
{
return
`#note_
${
this
.
noteId
}
`
;
...
...
@@ -53,7 +53,6 @@
'
setTargetNoteHash
'
,
]),
handleToggle
()
{
this
.
isExpanded
=
!
this
.
isExpanded
;
this
.
$emit
(
'
toggleHandler
'
);
},
updateTargetNoteHash
()
{
...
...
app/assets/javascripts/notes/components/noteable_discussion.vue
View file @
85ff85f1
<
script
>
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
{
SYSTEM_NOTE
}
from
'
../constants
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
...
...
@@ -8,13 +10,19 @@
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
noteEditedText
from
'
./note_edited_text.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
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
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
{
components
:
{
noteableNote
,
diffWithNote
,
userAvatarLink
,
noteHeader
,
noteSignedOutWidget
,
...
...
@@ -23,8 +31,13 @@
placeholderNote
,
placeholderSystemNote
,
},
directives
:
{
tooltip
,
},
mixins
:
[
autosave
,
noteable
,
resolvable
,
],
props
:
{
note
:
{
...
...
@@ -35,14 +48,25 @@
data
()
{
return
{
isReplying
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
};
},
computed
:
{
...
mapGetters
([
'
getNoteableData
'
,
'
discussionCount
'
,
'
resolvedDiscussionCount
'
,
'
unresolvedDiscussions
'
,
]),
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
()
{
return
this
.
discussion
.
author
;
...
...
@@ -71,26 +95,40 @@
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
()
{
if
(
this
.
isReplying
)
{
this
.
initAutoSave
();
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
}
},
updated
()
{
if
(
this
.
isReplying
)
{
if
(
!
this
.
autosave
)
{
this
.
initAutoSave
();
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
}
else
{
this
.
setAutoSave
();
}
}
},
created
()
{
this
.
resolveDiscussionsSvg
=
resolveDiscussionsSvg
;
this
.
nextDiscussionsSvg
=
nextDiscussionsSvg
;
},
methods
:
{
...
mapActions
([
'
saveNote
'
,
'
toggleDiscussion
'
,
'
removePlaceholderNotes
'
,
'
toggleResolveNote
'
,
]),
componentName
(
note
)
{
if
(
note
.
isPlaceholderNote
)
{
...
...
@@ -103,7 +141,7 @@
return
noteableNote
;
},
componentData
(
note
)
{
return
note
.
isPlaceholderNote
?
note
.
notes
[
0
]
:
note
;
return
note
.
isPlaceholderNote
?
this
.
note
.
notes
[
0
]
:
note
;
},
toggleDiscussionHandler
()
{
this
.
toggleDiscussion
({
discussionId
:
this
.
note
.
id
});
...
...
@@ -128,7 +166,7 @@
flashContainer
:
this
.
$el
,
data
:
{
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
target_type
:
'
issue
'
,
target_type
:
this
.
noteableType
,
target_id
:
this
.
discussion
.
noteable_id
,
note
:
{
note
:
noteText
},
},
...
...
@@ -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
>
<
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-icon"
>
<user-avatar-link
...
...
@@ -175,6 +228,7 @@ Please check your network connection and try again.`;
:created-at=
"discussion.created_at"
:note-id=
"discussion.id"
:include-toggle=
"true"
:expanded=
"note.expanded"
@
toggleHandler=
"toggleDiscussionHandler"
action-text=
"started a discussion"
class=
"discussion"
...
...
@@ -187,43 +241,103 @@ Please check your network connection and try again.`;
class-name=
"discussion-headline-light js-discussion-headline"
/>
</div>
</div>
<div
v-if=
"note.expanded"
class=
"discussion-body"
>
<div
class=
"panel panel-default"
>
<div
class=
"discussion-notes"
>
<ul
class=
"notes"
>
<component
v-for=
"note in note.notes"
:is=
"componentName(note)"
:note=
"componentData(note)"
:key=
"note.id"
/>
</ul>
<div
:class=
"
{ 'is-replying': isReplying }"
class="discussion-reply-holder">
<button
v-if=
"canReply && !isReplying"
@
click=
"showReplyForm"
type=
"button"
class=
"js-vue-discussion-reply btn btn-text-field"
title=
"Add a reply"
>
Reply...
</button>
<note-form
v-if=
"isReplying"
save-button-title=
"Comment"
:discussion=
"note"
:is-editing=
"false"
@
handleFormUpdate=
"saveReply"
@
cancelFormEdition=
"cancelReplyForm"
ref=
"noteForm"
/>
<note-signed-out-widget
v-if=
"!canReply"
/>
<div
v-if=
"note.expanded"
class=
"discussion-body"
>
<component
:is=
"wrapperComponent"
:discussion=
"discussion"
:class=
"wrapperClass"
>
<div
class=
"discussion-notes"
>
<ul
class=
"notes"
>
<component
v-for=
"note in note.notes"
:is=
"componentName(note)"
:note=
"componentData(note)"
:key=
"note.id"
/>
</ul>
<div
:class=
"
{ 'is-replying': isReplying }"
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
@
click=
"showReplyForm"
type=
"button"
class=
"js-vue-discussion-reply btn btn-text-field"
title=
"Add a reply"
>
Reply...
</button>
</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>
</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
v-if=
"isReplying"
save-button-title=
"Comment"
:note=
"note"
:is-editing=
"false"
@
handleFormUpdate=
"saveReply"
@
cancelFormEdition=
"cancelReplyForm"
ref=
"noteForm"
/>
<note-signed-out-widget
v-if=
"!canReply"
/>
</div>
</div>
</
div
>
</
component
>
</div>
</div>
</div>
...
...
app/assets/javascripts/notes/components/noteable_note.vue
View file @
85ff85f1
...
...
@@ -7,6 +7,8 @@
import
noteActions
from
'
./note_actions.vue
'
;
import
noteBody
from
'
./note_body.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
export
default
{
components
:
{
...
...
@@ -15,6 +17,10 @@
noteActions
,
noteBody
,
},
mixins
:
[
noteable
,
resolvable
,
],
props
:
{
note
:
{
type
:
Object
,
...
...
@@ -26,6 +32,7 @@
isEditing
:
false
,
isDeleting
:
false
,
isRequesting
:
false
,
isResolving
:
false
,
};
},
computed
:
{
...
...
@@ -65,6 +72,7 @@
...
mapActions
([
'
deleteNote
'
,
'
updateNote
'
,
'
toggleResolveNote
'
,
'
scrollToNoteIfNeeded
'
,
]),
editHandler
()
{
...
...
@@ -89,7 +97,7 @@
const
data
=
{
endpoint
:
this
.
note
.
path
,
note
:
{
target_type
:
'
issue
'
,
target_type
:
this
.
noteableType
,
target_id
:
this
.
note
.
noteable_id
,
note
:
{
note
:
noteText
},
},
...
...
@@ -134,7 +142,7 @@
// 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
.
note
.
note
=
noteText
;
this
.
$refs
.
noteBody
.
$refs
.
noteForm
.
note
=
noteText
;
this
.
$refs
.
noteBody
.
$refs
.
noteForm
.
note
.
note
=
noteText
;
},
},
};
...
...
@@ -171,8 +179,13 @@
:can-delete=
"note.current_user.can_edit"
:can-report-as-abuse=
"canReportAsAbuse"
:report-abuse-path=
"note.report_abuse_path"
:resolvable=
"note.resolvable"
:is-resolved=
"note.resolved"
:is-resolving=
"isResolving"
:resolved-by=
"note.resolved_by"
@
handleEdit=
"editHandler"
@
handleDelete=
"deleteHandler"
@
handleResolve=
"resolveHandler"
/>
</div>
<note-body
...
...
app/assets/javascripts/notes/components/notes_app.vue
View file @
85ff85f1
...
...
@@ -11,6 +11,7 @@
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
skeletonLoadingContainer
from
'
../../vue_shared/components/notes/skeleton_note.vue
'
;
export
default
{
name
:
'
NotesApp
'
,
...
...
@@ -48,7 +49,24 @@
...
mapGetters
([
'
notes
'
,
'
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
()
{
this
.
setNotesData
(
this
.
notesData
);
...
...
@@ -67,6 +85,10 @@
this
.
actionToggleAward
({
awardName
,
noteId
});
});
}
document
.
addEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
},
beforeDestroy
()
{
document
.
removeEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
},
methods
:
{
...
mapActions
({
...
...
@@ -81,6 +103,9 @@
setTargetNoteHash
:
'
setTargetNoteHash
'
,
}),
getComponentName
(
note
)
{
if
(
note
.
isSkeletonNote
)
{
return
skeletonLoadingContainer
;
}
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
constants
.
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
...
...
@@ -109,9 +134,14 @@
});
},
initPolling
()
{
if
(
this
.
isPollingInitialized
)
{
return
;
}
this
.
setLastFetchedAt
(
this
.
getNotesDataByProp
(
'
lastFetchedAt
'
));
this
.
poll
();
this
.
isPollingInitialized
=
true
;
},
checkLocationHash
()
{
const
hash
=
getLocationHash
();
...
...
@@ -128,25 +158,20 @@
<
template
>
<div
id=
"notes"
>
<div
v-if=
"isLoading"
class=
"js-loading loading"
>
<loading-icon
/>
</div>
<ul
v-if=
"!isLoading"
id=
"notes-list"
class=
"notes main-notes-list timeline"
>
<component
v-for=
"note in
n
otes"
v-for=
"note in
allN
otes"
:is=
"getComponentName(note)"
:note=
"getComponentData(note)"
:key=
"note.id"
/>
</ul>
<comment-form
/>
<comment-form
:noteable-type=
"noteableType"
/>
</div>
</
template
>
app/assets/javascripts/notes/constants.js
View file @
85ff85f1
export
const
DISCUSSION_NOTE
=
'
DiscussionNote
'
;
export
const
DIFF_NOTE
=
'
DiffNote
'
;
export
const
DISCUSSION
=
'
discussion
'
;
export
const
NOTE
=
'
note
'
;
export
const
SYSTEM_NOTE
=
'
systemNote
'
;
...
...
@@ -8,4 +9,7 @@ export const REOPENED = 'reopened';
export
const
CLOSED
=
'
closed
'
;
export
const
EMOJI_THUMBSUP
=
'
thumbsup
'
;
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 @
85ff85f1
...
...
@@ -20,17 +20,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
,
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
,
},
notesData
:
JSON
.
parse
(
notesDataset
.
notesData
),
};
},
render
(
createElement
)
{
...
...
app/assets/javascripts/notes/mixins/autosave.js
View file @
85ff85f1
import
Autosave
from
'
../../autosave
'
;
import
{
capitalizeFirstCharacter
}
from
'
../../lib/utils/text_utility
'
;
export
default
{
methods
:
{
initAutoSave
()
{
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
noteForm
.
$refs
.
textarea
),
[
'
Note
'
,
'
Issue
'
,
this
.
note
.
id
],
'
issue
'
);
initAutoSave
(
noteableType
)
{
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
noteForm
.
$refs
.
textarea
),
[
'
Note
'
,
capitalizeFirstCharacter
(
noteableType
),
this
.
note
.
id
]
);
},
resetAutoSave
()
{
this
.
autosave
.
reset
();
...
...
app/assets/javascripts/notes/mixins/noteable.js
0 → 100644
View file @
85ff85f1
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 @
85ff85f1
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 @
85ff85f1
import
Vue
from
'
vue
'
;
import
VueResource
from
'
vue-resource
'
;
import
*
as
constants
from
'
../constants
'
;
Vue
.
use
(
VueResource
);
...
...
@@ -19,6 +20,12 @@ export default {
createNewNote
(
endpoint
,
data
)
{
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
=
{})
{
const
{
endpoint
,
lastFetchedAt
}
=
data
;
const
options
=
{
...
...
app/assets/javascripts/notes/stores/actions.js
View file @
85ff85f1
...
...
@@ -61,8 +61,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export
const
removePlaceholderNotes
=
({
commit
})
=>
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
.
toggleIssueState
(
state
.
notesData
.
close
Issue
Path
)
.
toggleIssueState
(
state
.
notesData
.
closePath
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
commit
(
types
.
CLOSE_ISSUE
);
...
...
@@ -70,7 +79,7 @@ export const closeIssue = ({ 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
((
data
)
=>
{
commit
(
types
.
REOPEN_ISSUE
);
...
...
@@ -80,7 +89,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => service
export
const
emitStateChangedEvent
=
({
commit
,
getters
},
data
)
=>
{
const
event
=
new
CustomEvent
(
'
issuable_vue_app:change
'
,
{
detail
:
{
data
,
isClosed
:
getters
.
issue
State
===
constants
.
CLOSED
,
isClosed
:
getters
.
open
State
===
constants
.
CLOSED
,
}
});
document
.
dispatchEvent
(
event
);
...
...
@@ -174,7 +183,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
resp
.
notes
.
forEach
((
note
)
=>
{
if
(
notesById
[
note
.
id
])
{
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
);
if
(
discussion
)
{
...
...
app/assets/javascripts/notes/stores/getters.js
View file @
85ff85f1
...
...
@@ -8,7 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export
const
getNoteableData
=
state
=>
state
.
noteableData
;
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
getUserDataByProp
=
state
=>
prop
=>
state
.
userData
&&
state
.
userData
[
prop
];
...
...
@@ -30,3 +30,37 @@ export const getCurrentUserLastNote = state => _.flatten(
export
const
getDiscussionLastNote
=
state
=>
discussion
=>
reverseNotes
(
discussion
.
notes
)
.
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 @
85ff85f1
...
...
@@ -12,6 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export
const
TOGGLE_AWARD
=
'
TOGGLE_AWARD
'
;
export
const
TOGGLE_DISCUSSION
=
'
TOGGLE_DISCUSSION
'
;
export
const
UPDATE_NOTE
=
'
UPDATE_NOTE
'
;
export
const
UPDATE_DISCUSSION
=
'
UPDATE_DISCUSSION
'
;
// Issue
export
const
CLOSE_ISSUE
=
'
CLOSE_ISSUE
'
;
...
...
app/assets/javascripts/notes/stores/mutations.js
View file @
85ff85f1
import
*
as
utils
from
'
./utils
'
;
import
*
as
types
from
'
./mutation_types
'
;
import
*
as
constants
from
'
../constants
'
;
import
{
isInMRPage
}
from
'
../../lib/utils/common_utils
'
;
export
default
{
[
types
.
ADD_NEW_NOTE
](
state
,
note
)
{
const
{
discussion_id
,
type
}
=
note
;
const
[
exists
]
=
state
.
notes
.
filter
(
n
=>
n
.
id
===
note
.
discussion_id
);
const
isDiscussion
=
(
type
===
constants
.
DISCUSSION_NOTE
);
if
(
!
exists
)
{
const
noteData
=
{
expanded
:
true
,
id
:
discussion_id
,
individual_note
:
!
(
type
===
constants
.
DISCUSSION_NOTE
)
,
individual_note
:
!
isDiscussion
,
notes
:
[
note
],
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
);
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
}
},
...
...
@@ -25,6 +35,7 @@ export default {
if
(
noteObj
)
{
noteObj
.
notes
.
push
(
note
);
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
}
},
...
...
@@ -41,6 +52,8 @@ export default {
state
.
notes
.
splice
(
state
.
notes
.
indexOf
(
noteObj
),
1
);
}
}
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
},
[
types
.
REMOVE_PLACEHOLDER_NOTES
](
state
)
{
...
...
@@ -77,15 +90,19 @@ export default {
const
notes
=
[];
notesData
.
forEach
((
note
)
=>
{
const
nn
=
Object
.
assign
({},
note
);
// To support legacy notes, should be very rare case.
if
(
note
.
individual_note
&&
note
.
notes
.
length
>
1
)
{
note
.
notes
.
forEach
((
n
)
=>
{
const
nn
=
Object
.
assign
({},
note
);
nn
.
notes
=
[
n
];
// override notes array to only have one item to mimick individual_note
notes
.
push
(
nn
);
});
}
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 {
user
:
{
id
,
name
,
username
},
});
}
document
.
dispatchEvent
(
new
CustomEvent
(
'
refreshLegacyNotes
'
));
},
[
types
.
TOGGLE_DISCUSSION
](
state
,
{
discussionId
})
{
...
...
@@ -151,6 +170,24 @@ export default {
const
comment
=
utils
.
findNoteObjectById
(
noteObj
.
notes
,
note
.
id
);
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
)
{
...
...
app/assets/javascripts/notes/stores/utils.js
View file @
85ff85f1
...
...
@@ -28,4 +28,3 @@ export const getQuickActionText = (note) => {
export
const
hasQuickActions
=
note
=>
REGEX_QUICK_ACTIONS
.
test
(
note
);
export
const
stripQuickActions
=
note
=>
note
.
replace
(
REGEX_QUICK_ACTIONS
,
''
).
trim
();
app/assets/javascripts/pages/projects/merge_requests/show/index.js
View file @
85ff85f1
...
...
@@ -20,7 +20,6 @@ document.addEventListener('DOMContentLoaded', () => {
initPipelines
();
const
mrShowNode
=
document
.
querySelector
(
'
.merge-request
'
);
window
.
mergeRequest
=
new
MergeRequest
({
action
:
mrShowNode
.
dataset
.
mrAction
,
});
...
...
app/assets/javascripts/vue_shared/components/clipboard_button.vue
View file @
85ff85f1
<
script
>
import
tooltip
from
'
../directives/tooltip
'
;
/**
* Falls back to the code used in `copy_to_clipboard.js`
*/
import
tooltip
from
'
../directives/tooltip
'
;
export
default
{
name
:
'
ClipboardButton
'
,
...
...
app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
View file @
85ff85f1
...
...
@@ -11,14 +11,12 @@
default
:
false
,
required
:
false
,
},
isConfidential
:
{
type
:
Boolean
,
default
:
false
,
required
:
false
,
},
},
computed
:
{
warningIcon
()
{
if
(
this
.
isConfidential
)
return
'
eye-slash
'
;
...
...
@@ -26,7 +24,6 @@
return
''
;
},
isLockedAndConfidential
()
{
return
this
.
isConfidential
&&
this
.
isLocked
;
},
...
...
app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
0 → 100644
View file @
85ff85f1
<
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 @
85ff85f1
...
...
@@ -723,7 +723,7 @@ ul.notes {
.line-resolve-all
{
vertical-align
:
middle
;
display
:
inline-block
;
padding
:
5px
10px
6
px
;
padding
:
6px
10
px
;
background-color
:
$gray-light
;
border
:
1px
solid
$border-color
;
border-radius
:
$border-radius-default
;
...
...
app/controllers/concerns/issuable_actions.rb
View file @
85ff85f1
...
...
@@ -77,6 +77,20 @@ module IssuableActions
render
json:
{
notice:
"
#{
quantity
}
#{
resource_name
.
pluralize
(
quantity
)
}
updated"
}
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
def
recaptcha_check_if_spammable
(
should_redirect
=
true
,
&
block
)
...
...
app/controllers/concerns/notes_actions.rb
View file @
85ff85f1
...
...
@@ -22,7 +22,7 @@ module NotesActions
notes
=
notes
.
reject
{
|
n
|
n
.
cross_reference_not_visible_for?
(
current_user
)
}
notes_json
[
:notes
]
=
if
noteable
.
discussions_rendered_on_frontend
?
if
use_note_serializer
?
note_serializer
.
represent
(
notes
)
else
notes
.
map
{
|
note
|
note_json
(
note
)
}
...
...
@@ -95,7 +95,7 @@ module NotesActions
if
note
.
persisted?
attrs
[
:valid
]
=
true
if
noteable
.
discussions_rendered_on_frontend
?
if
use_note_serializer
?
attrs
.
merge!
(
note_serializer
.
represent
(
note
))
else
attrs
.
merge!
(
...
...
@@ -233,4 +233,14 @@ module NotesActions
the_project
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
app/controllers/projects/discussions_controller.rb
View file @
85ff85f1
class
Projects::DiscussionsController
<
Projects
::
ApplicationController
include
NotesHelper
include
RendersNotes
before_action
:check_merge_requests_available!
before_action
:merge_request
before_action
:discussion
...
...
@@ -7,22 +10,45 @@ class Projects::DiscussionsController < Projects::ApplicationController
def
resolve
Discussions
::
ResolveService
.
new
(
project
,
current_user
,
merge_request:
merge_request
).
execute
(
discussion
)
render
json:
{
resolved_by:
discussion
.
resolved_by
.
try
(
:name
),
discussion_headline_html:
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
}
render_discussion
end
def
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:
{
resolved_by:
discussion
.
resolved_by
.
try
(
:name
),
discussion_headline_html:
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
}
end
private
def
merge_request
@merge_request
||=
MergeRequestsFinder
.
new
(
current_user
,
project_id:
@project
.
id
).
find_by!
(
iid:
params
[
:merge_request_id
])
end
...
...
app/controllers/projects/issues_controller.rb
View file @
85ff85f1
...
...
@@ -64,20 +64,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with
(
@issue
)
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
create_params
=
issue_params
.
merge
(
spammable_params
).
merge
(
merge_request_to_resolve_discussions_of:
params
[
:merge_request_to_resolve_discussions_of
],
...
...
app/controllers/projects/notes_controller.rb
View file @
85ff85f1
class
Projects::NotesController
<
Projects
::
ApplicationController
include
NotesActions
include
NotesHelper
include
ToggleAwardEmoji
before_action
:whitelist_query_limiting
,
only:
[
:create
]
...
...
@@ -38,10 +39,14 @@ class Projects::NotesController < Projects::ApplicationController
discussion
=
note
.
discussion
render
json:
{
resolved_by:
note
.
resolved_by
.
try
(
:name
),
discussion_headline_html:
(
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
if
discussion
)
}
if
serialize_notes?
render_json_with_notes_serializer
else
render
json:
{
resolved_by:
note
.
resolved_by
.
try
(
:name
),
discussion_headline_html:
(
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
if
discussion
)
}
end
end
def
unresolve
...
...
@@ -51,16 +56,27 @@ class Projects::NotesController < Projects::ApplicationController
discussion
=
note
.
discussion
render
json:
{
discussion_headline_html:
(
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
if
discussion
)
}
if
serialize_notes?
render_json_with_notes_serializer
else
render
json:
{
discussion_headline_html:
(
view_to_html_string
(
'discussions/_headline'
,
discussion:
discussion
)
if
discussion
)
}
end
end
private
def
render_json_with_notes_serializer
Notes
::
RenderService
.
new
(
current_user
).
execute
([
note
],
project
)
render
json:
note_serializer
.
represent
(
note
)
end
def
note
@note
||=
@project
.
notes
.
find
(
params
[
:id
])
end
alias_method
:awardable
,
:note
def
finder_params
...
...
app/helpers/notes_helper.rb
View file @
85ff85f1
...
...
@@ -151,7 +151,38 @@ module NotesHelper
}
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
)
discussion
.
resolved_by_push?
?
'Automatically resolved'
:
'Resolved'
end
def
has_vue_discussions_cookie?
cookies
[
:vue_mr_discussions
]
==
'true'
end
def
serialize_notes?
has_vue_discussions_cookie?
&&
!
params
[
'html'
]
end
end
app/models/note.rb
View file @
85ff85f1
...
...
@@ -137,6 +137,7 @@ class Note < ActiveRecord::Base
def
find_discussion
(
discussion_id
)
notes
=
where
(
discussion_id:
discussion_id
).
fresh
.
to_a
return
if
notes
.
empty?
Discussion
.
build
(
notes
)
...
...
app/serializers/diff_file_entity.rb
0 → 100644
View file @
85ff85f1
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 @
85ff85f1
...
...
@@ -7,4 +7,42 @@ class DiscussionEntity < Grape::Entity
expose
:notes
,
using:
NoteEntity
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
app/serializers/merge_request_widget_entity.rb
View file @
85ff85f1
...
...
@@ -134,6 +134,14 @@ class MergeRequestWidgetEntity < IssuableEntity
expose
:can_cherry_pick_on_current_merge_request
do
|
merge_request
|
presenter
(
merge_request
).
can_cherry_pick_on_current_merge_request?
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
# Paths
...
...
@@ -208,6 +216,10 @@ class MergeRequestWidgetEntity < IssuableEntity
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
|
commit_change_content_project_merge_request_path
(
merge_request
.
project
,
merge_request
)
end
...
...
app/serializers/note_entity.rb
View file @
85ff85f1
...
...
@@ -23,6 +23,10 @@ class NoteEntity < API::Entities::Note
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
|
SystemNoteHelper
.
system_note_icon_name
(
note
)
end
...
...
@@ -53,6 +57,14 @@ class NoteEntity < API::Entities::Note
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
:delete_attachment_path
,
if:
->
(
note
,
_
)
{
note
.
attachment?
}
do
|
note
|
delete_attachment_project_note_path
(
note
.
project
,
note
)
...
...
app/views/projects/issues/_discussion.html.haml
View file @
85ff85f1
...
...
@@ -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'
%section
.js-vue-notes-event
#js-vue-notes
{
data:
{
discussions_path:
discussions_project_issue_path
(
@project
,
@issue
,
format: :json
),
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
),
current_user_data:
UserSerializer
.
new
.
represent
(
current_user
,
only_path:
true
).
to_json
}
}
#js-vue-notes
{
data:
{
notes_data:
notes_data
(
@issue
),
noteable_data:
serialize_issuable
(
@issue
),
current_user_data:
UserSerializer
.
new
.
represent
(
current_user
,
only_path:
true
).
to_json
}
}
app/views/projects/issues/show.html.haml
View file @
85ff85f1
...
...
@@ -86,7 +86,7 @@
.content-block.emoji-block
.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
.col-sm-4.new-branch-col
=
render
'new_branch'
unless
@issue
.
confidential?
...
...
app/views/projects/merge_requests/show.html.haml
View file @
85ff85f1
-
@gfm_form
=
true
-
@content_class
=
"limit-container-width"
unless
fluid_layout
-
add_to_breadcrumbs
"Merge Requests"
,
project_merge_requests_path
(
@project
)
-
breadcrumb_title
@merge_request
.
to_reference
...
...
@@ -8,6 +9,9 @@
=
webpack_bundle_tag
(
'common_vue'
)
=
webpack_bundle_tag
(
'issuable'
)
-
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
)
}
}
=
render
"projects/merge_requests/mr_title"
...
...
@@ -31,7 +35,7 @@
#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
.merge-request-tabs-holder
{
class:
(
"js-tabs-affix"
unless
ENV
[
'RAILS_ENV'
]
==
'test'
)
}
...
...
@@ -59,28 +63,37 @@
=
tab_link_for
@merge_request
,
:diffs
do
Changes
%span
.badge
=
@merge_request
.
diff_size
#resolve-count-app
.line-resolve-all-container.prepend-top-10
{
"v-cloak"
=>
true
}
%resolve-count
{
"inline-template"
=>
true
,
":logged-out"
=>
"#{current_user.nil?}"
}
%div
.line-resolve-all
{
"v-show"
=>
"discussionCount > 0"
,
":class"
=>
"{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }"
}
%span
.line-resolve-btn.is-disabled
{
type:
"button"
,
":class"
=>
"{ 'is-active': resolvedDiscussionCount === discussionCount }"
}
%template
{
'v-if'
=>
'resolvedDiscussionCount === discussionCount'
}
=
render
'shared/icons/icon_status_success_solid.svg'
%template
{
'v-else'
=>
''
}
=
render
'shared/icons/icon_resolve_discussion.svg'
%span
.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
=
render
"discussions/new_issue_for_all_discussions"
,
merge_request:
@merge_request
=
render
"discussions/jump_to_next"
-
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
{
"inline-template"
=>
true
,
":logged-out"
=>
"#{current_user.nil?}"
}
%div
.line-resolve-all
{
"v-show"
=>
"discussionCount > 0"
,
":class"
=>
"{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }"
}
%span
.line-resolve-btn.is-disabled
{
type:
"button"
,
":class"
=>
"{ 'is-active': resolvedDiscussionCount === discussionCount }"
}
%template
{
'v-if'
=>
'resolvedDiscussionCount === discussionCount'
}
=
render
'shared/icons/icon_status_success_solid.svg'
%template
{
'v-else'
=>
''
}
=
render
'shared/icons/icon_resolve_discussion.svg'
%span
.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
=
render
"discussions/new_issue_for_all_discussions"
,
merge_request:
@merge_request
=
render
"discussions/jump_to_next"
.tab-content
#diff-notes-app
#notes
.notes.tab-pane.voting_notes
.row
%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"
-
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
-# This tab is always loaded via AJAX
...
...
app/views/shared/notes/_notes_with_form.html.haml
View file @
85ff85f1
-
issuable
=
@issue
||
@merge_request
-
discussion_locked
=
issuable
&
.
discussion_locked?
%ul
#notes-list
.notes.main-notes-list.timeline
=
render
"shared/notes/notes"
-
unless
has_vue_discussions_cookie?
%ul
#notes-list
.notes.main-notes-list.timeline
=
render
"shared/notes/notes"
=
render
'shared/notes/edit_form'
,
project:
@project
-
if
can_create_note?
%ul
.notes.notes-form.timeline
%ul
.notes.notes-form.timeline
{
:class
=>
(
'hidden'
if
has_vue_discussions_cookie?
)
}
%li
.timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
...
...
config/routes/project.rb
View file @
85ff85f1
...
...
@@ -112,6 +112,7 @@ constraints(ProjectUrlConstrainer.new) do
post
:remove_wip
post
:assign_related_issues
get
:discussions
,
format: :json
post
:rebase
scope
constraints:
{
format:
nil
},
action: :show
do
...
...
config/webpack.config.js
View file @
85ff85f1
...
...
@@ -57,6 +57,7 @@ function generateEntries() {
help
:
'
./help/help.js
'
,
merge_conflicts
:
'
./merge_conflicts/merge_conflicts_bundle.js
'
,
monitoring
:
'
./monitoring/monitoring_bundle.js
'
,
mr_notes
:
'
./mr_notes/index.js
'
,
notebook_viewer
:
'
./blob/notebook_viewer.js
'
,
pdf_viewer
:
'
./blob/pdf_viewer.js
'
,
pipelines_details
:
'
./pipelines/pipeline_details_bundle.js
'
,
...
...
@@ -264,6 +265,7 @@ var config = {
'
groups
'
,
'
merge_conflicts
'
,
'
monitoring
'
,
'
mr_notes
'
,
'
notebook_viewer
'
,
'
pdf_viewer
'
,
'
pipelines
'
,
...
...
spec/controllers/projects/discussions_controller_spec.rb
View file @
85ff85f1
...
...
@@ -71,6 +71,19 @@ describe Projects::DiscussionsController do
expect
(
response
).
to
have_gitlab_http_status
(
200
)
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
...
...
@@ -119,6 +132,19 @@ describe Projects::DiscussionsController do
expect
(
response
).
to
have_gitlab_http_status
(
200
)
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
...
...
spec/controllers/projects/issues_controller_spec.rb
View file @
85ff85f1
...
...
@@ -974,7 +974,7 @@ describe Projects::IssuesController do
it
'returns discussion json'
do
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
it
'filters notes that the user should not see'
do
...
...
spec/features/merge_request/user_posts_notes_spec.rb
View file @
85ff85f1
...
...
@@ -144,7 +144,7 @@ describe 'Merge request > User posts notes', :js do
end
end
describe
'deleting a
n attachment
'
do
describe
'deleting a
ttachment on legacy diff note
'
do
before
do
find
(
'.note'
).
hover
...
...
spec/fixtures/api/schemas/entities/merge_request_widget.json
View file @
85ff85f1
...
...
@@ -76,7 +76,9 @@
"properties"
:
{
"can_remove_source_branch"
:
{
"type"
:
"boolean"
},
"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
},
...
...
@@ -104,6 +106,7 @@
"merge_ongoing"
:
{
"type"
:
"boolean"
},
"ff_only_enabled"
:
{
"type"
:
[
"boolean"
,
false
]
},
"should_be_rebased"
:
{
"type"
:
"boolean"
},
"create_note_path"
:
{
"type"
:
[
"string"
,
"null"
]
},
"rebase_commit_sha"
:
{
"type"
:
[
"string"
,
"null"
]
},
"rebase_in_progress"
:
{
"type"
:
"boolean"
},
"can_push_to_source_branch"
:
{
"type"
:
"boolean"
},
...
...
spec/javascripts/autosave_spec.js
View file @
85ff85f1
...
...
@@ -3,28 +3,24 @@ import AccessorUtilities from '~/lib/utils/accessor';
describe
(
'
Autosave
'
,
()
=>
{
let
autosave
;
const
field
=
$
(
'
<textarea></textarea>
'
);
const
key
=
'
key
'
;
describe
(
'
class constructor
'
,
()
=>
{
const
key
=
'
key
'
;
const
field
=
jasmine
.
createSpyObj
(
'
field
'
,
[
'
data
'
,
'
on
'
]);
beforeEach
(()
=>
{
spyOn
(
AccessorUtilities
,
'
isLocalStorageAccessSafe
'
).
and
.
returnValue
(
true
);
spyOn
(
Autosave
.
prototype
,
'
restore
'
);
autosave
=
new
Autosave
(
field
,
key
);
});
it
(
'
should set .isLocalStorageAvailable
'
,
()
=>
{
autosave
=
new
Autosave
(
field
,
key
);
expect
(
AccessorUtilities
.
isLocalStorageAccessSafe
).
toHaveBeenCalled
();
expect
(
autosave
.
isLocalStorageAvailable
).
toBe
(
true
);
});
});
describe
(
'
restore
'
,
()
=>
{
const
key
=
'
key
'
;
const
field
=
jasmine
.
createSpyObj
(
'
field
'
,
[
'
trigger
'
]);
beforeEach
(()
=>
{
autosave
=
{
field
,
...
...
@@ -49,24 +45,53 @@ describe('Autosave', () => {
describe
(
'
if .isLocalStorageAvailable is `true`
'
,
()
=>
{
beforeEach
(()
=>
{
autosave
.
isLocalStorageAvailable
=
true
;
Autosave
.
prototype
.
restore
.
call
(
autosave
);
});
it
(
'
should call .getItem
'
,
()
=>
{
Autosave
.
prototype
.
restore
.
call
(
autosave
);
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
(
'
if field gets deleted from DOM
'
,
()
=>
{
beforeEach
(()
=>
{
autosave
.
field
=
$
(
'
.not-a-real-element
'
);
});
it
(
'
does not trigger event
'
,
()
=>
{
spyOn
(
field
,
'
trigger
'
).
and
.
callThrough
();
expect
(
field
.
trigger
,
).
not
.
toHaveBeenCalled
();
});
});
});
describe
(
'
save
'
,
()
=>
{
const
field
=
jasmine
.
createSpyObj
(
'
field
'
,
[
'
val
'
]);
beforeEach
(()
=>
{
autosave
=
jasmine
.
createSpyObj
(
'
autosave
'
,
[
'
reset
'
]);
autosave
.
field
=
field
;
field
.
val
.
and
.
returnValue
(
'
value
'
);
field
.
val
(
'
value
'
);
spyOn
(
window
.
localStorage
,
'
setItem
'
);
});
...
...
@@ -97,8 +122,6 @@ describe('Autosave', () => {
});
describe
(
'
reset
'
,
()
=>
{
const
key
=
'
key
'
;
beforeEach
(()
=>
{
autosave
=
{
key
,
...
...
spec/javascripts/fixtures/merge_requests.rb
View file @
85ff85f1
...
...
@@ -70,8 +70,50 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_merge_request
(
example
.
description
,
merge_request
)
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
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
)
get
:show
,
namespace_id:
project
.
namespace
.
to_param
,
...
...
spec/javascripts/notes/components/comment_form_spec.js
View file @
85ff85f1
import
Vue
from
'
vue
'
;
import
Autosize
from
'
autosize
'
;
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
{
keyboardDownEvent
}
from
'
../../issue_show/helpers
'
;
describe
(
'
issue_comment_form component
'
,
()
=>
{
let
vm
;
const
Component
=
Vue
.
extend
(
issue
CommentForm
);
const
Component
=
Vue
.
extend
(
CommentForm
);
let
mountComponent
;
beforeEach
(()
=>
{
mountComponent
=
()
=>
new
Component
({
mountComponent
=
(
noteableType
=
'
issue
'
)
=>
new
Component
({
propsData
:
{
noteableType
,
},
store
,
}).
$mount
();
});
...
...
@@ -136,6 +139,11 @@ describe('issue_comment_form component', () => {
expect
(
vm
.
editCurrentUserLastNote
).
toHaveBeenCalled
();
});
it
(
'
inits autosave
'
,
()
=>
{
expect
(
vm
.
autosave
).
toBeDefined
();
expect
(
vm
.
autosave
.
key
).
toEqual
(
`autosave/Note/Issue/
${
noteableDataMock
.
id
}
`
);
});
});
describe
(
'
event enter
'
,
()
=>
{
...
...
@@ -182,6 +190,15 @@ describe('issue_comment_form component', () => {
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
'
,
()
=>
{
...
...
spec/javascripts/notes/components/diff_file_header_spec.js
0 → 100644
View file @
85ff85f1
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 @
85ff85f1
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 @
85ff85f1
...
...
@@ -24,6 +24,7 @@ describe('note_app', () => {
beforeEach
(()
=>
{
jasmine
.
addMatchers
(
vueMatchers
);
$
(
'
body
'
).
attr
(
'
data-page
'
,
'
projects:merge_requests:show
'
);
const
IssueNotesApp
=
Vue
.
extend
(
notesApp
);
...
...
@@ -119,8 +120,8 @@ describe('note_app', () => {
vm
=
mountComponent
();
});
it
(
'
should render loading icon
'
,
()
=>
{
expect
(
vm
).
toIncludeElement
(
'
.
js-loading
'
);
it
(
'
renders skeleton notes
'
,
()
=>
{
expect
(
vm
).
toIncludeElement
(
'
.
animation-container
'
);
});
it
(
'
should render form
'
,
()
=>
{
...
...
spec/javascripts/notes/components/note_body_spec.js
View file @
85ff85f1
...
...
@@ -30,17 +30,26 @@ describe('issue_note_body component', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.note-text
'
).
innerHTML
).
toEqual
(
note
.
note_html
);
});
it
(
'
should be render form if user is editing
'
,
(
done
)
=>
{
vm
.
isEditing
=
true
;
it
(
'
should render awards list
'
,
()
=>
{
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
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
textarea.js-task-list-field
'
)).
toBeDefined
();
done
();
describe
(
'
isEditing
'
,
()
=>
{
beforeEach
((
done
)
=>
{
vm
.
isEditing
=
true
;
Vue
.
nextTick
(
done
);
});
});
it
(
'
should render awards list
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-awards-block button [data-name="baseball"]
'
)).
toBeDefined
();
expect
(
vm
.
$el
.
querySelector
(
'
.js-awards-block button [data-name="bath_tone3"]
'
)).
toBeDefined
();
it
(
'
renders edit form
'
,
()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
textarea.js-task-list-field
'
)).
not
.
toBeNull
();
});
it
(
'
adds autosave
'
,
()
=>
{
const
autosaveKey
=
`autosave/Note/
${
note
.
noteable_type
}
/
${
note
.
id
}
`
;
expect
(
vm
.
autosave
).
toExist
();
expect
(
vm
.
autosave
.
key
).
toEqual
(
autosaveKey
);
});
});
});
spec/javascripts/notes/components/note_header_spec.js
View file @
85ff85f1
...
...
@@ -32,6 +32,7 @@ describe('note_header component', () => {
createdAt
:
'
2017-08-02T10:51:58.559Z
'
,
includeToggle
:
false
,
noteId
:
1394
,
expanded
:
true
,
},
}).
$mount
();
});
...
...
@@ -68,6 +69,7 @@ describe('note_header component', () => {
createdAt
:
'
2017-08-02T10:51:58.559Z
'
,
includeToggle
:
true
,
noteId
:
1395
,
expanded
:
true
,
},
}).
$mount
();
});
...
...
@@ -76,17 +78,35 @@ describe('note_header component', () => {
expect
(
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button
'
)).
toBeDefined
();
});
it
(
'
should toggle the disucssion icon
'
,
(
done
)
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button i
'
).
classList
.
contains
(
'
fa-chevron-up
'
),
).
toEqual
(
true
);
it
(
'
emits toggle event on click
'
,
(
done
)
=>
{
spyOn
(
vm
,
'
$emit
'
);
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
(()
=>
{
expect
(
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button i
'
).
classList
.
contains
(
'
fa-chevron-down
'
)
,
).
to
Equal
(
true
);
vm
.
$el
.
querySelector
(
'
.js-vue-toggle-button i
'
).
classList
,
).
to
Contain
(
'
fa-chevron-down
'
);
done
();
});
});
...
...
spec/javascripts/notes/mock_data.js
View file @
85ff85f1
...
...
@@ -7,8 +7,9 @@ export const notesDataMock = {
notesPath
:
'
/gitlab-org/gitlab-ce/noteable/issue/98/notes
'
,
quickActionsDocsPath
:
'
/help/user/project/quick_actions
'
,
registerPath
:
'
/users/sign_in?redirect_to_referer=yes#register-pane
'
,
closeIssuePath
:
'
/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close
'
,
reopenIssuePath
:
'
/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen
'
,
totalNotes
:
1
,
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
=
{
...
...
spec/javascripts/notes/stores/getters_spec.js
View file @
85ff85f1
...
...
@@ -56,9 +56,9 @@ describe('Getters Notes Store', () => {
});
});
describe
(
'
issue
State
'
,
()
=>
{
describe
(
'
open
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 @
85ff85f1
import
mutations
from
'
~/notes/stores/mutations
'
;
import
{
note
,
discussionMock
,
notesDataMock
,
userDataMock
,
noteableDataMock
,
individualNote
}
from
'
../mock_data
'
;
describe
(
'
Mutation Notes Store
'
,
()
=>
{
describe
(
'
Notes Store mutations
'
,
()
=>
{
describe
(
'
ADD_NEW_NOTE
'
,
()
=>
{
let
state
;
let
noteData
;
...
...
@@ -103,7 +103,8 @@ describe('Mutation Notes Store', () => {
};
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 @
85ff85f1
...
...
@@ -34,6 +34,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe
(
'
Notes
'
,
function
()
{
const
FLASH_TYPE_ALERT
=
'
alert
'
;
const
NOTES_POST_PATH
=
/
(
.*
)\/
notes
\?
html=true$/
;
var
commentsTemplate
=
'
merge_requests/merge_request_with_comment.html.raw
'
;
preloadFixtures
(
commentsTemplate
);
...
...
@@ -154,7 +155,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$form
.
find
(
'
textarea.js-note-text
'
).
val
(
sampleComment
);
mock
=
new
MockAdapter
(
axios
);
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
reply
(
200
,
noteEntity
);
mock
.
onPost
(
NOTES_POST_PATH
).
reply
(
200
,
noteEntity
);
});
afterEach
(()
=>
{
...
...
@@ -506,11 +507,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
let
mock
;
function
mockNotesPost
()
{
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
reply
(
200
,
note
);
mock
.
onPost
(
NOTES_POST_PATH
).
reply
(
200
,
note
);
}
function
mockNotesPostError
()
{
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
networkError
();
mock
.
onPost
(
NOTES_POST_PATH
).
networkError
();
}
beforeEach
(()
=>
{
...
...
@@ -631,7 +632,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
reply
(
200
,
note
);
mock
.
onPost
(
NOTES_POST_PATH
).
reply
(
200
,
note
);
this
.
notes
=
new
Notes
(
''
,
[]);
window
.
gon
.
current_username
=
'
root
'
;
...
...
@@ -684,7 +685,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
beforeEach
(()
=>
{
mock
=
new
MockAdapter
(
axios
);
mock
.
onPost
(
/
(
.*
)\/
notes$/
).
reply
(
200
,
note
);
mock
.
onPost
(
NOTES_POST_PATH
).
reply
(
200
,
note
);
this
.
notes
=
new
Notes
(
''
,
[]);
window
.
gon
.
current_username
=
'
root
'
;
...
...
spec/serializers/diff_file_entity_spec.rb
0 → 100644
View file @
85ff85f1
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 @
85ff85f1
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 @
85ff85f1
...
...
@@ -48,4 +48,15 @@ describe NoteEntity do
expect
(
subject
).
to
include
(
:system_note_icon_name
)
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
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