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
059ab73b
Commit
059ab73b
authored
Feb 28, 2018
by
Fatih Acet
Committed by
Jacob Schatz
Feb 28, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Render MR Notes with Vue with behind a cookie
parent
0be4a77d
Changes
74
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
74 changed files
with
1747 additions
and
327 deletions
+1747
-327
app/assets/javascripts/autosave.js
app/assets/javascripts/autosave.js
+14
-18
app/assets/javascripts/awards_handler.js
app/assets/javascripts/awards_handler.js
+12
-4
app/assets/javascripts/diff_notes/components/jump_to_discussion.js
...s/javascripts/diff_notes/components/jump_to_discussion.js
+1
-1
app/assets/javascripts/diff_notes/components/resolve_btn.js
app/assets/javascripts/diff_notes/components/resolve_btn.js
+1
-0
app/assets/javascripts/diff_notes/diff_notes_bundle.js
app/assets/javascripts/diff_notes/diff_notes_bundle.js
+9
-6
app/assets/javascripts/diff_notes/services/resolve.js
app/assets/javascripts/diff_notes/services/resolve.js
+3
-2
app/assets/javascripts/lib/utils/common_utils.js
app/assets/javascripts/lib/utils/common_utils.js
+16
-5
app/assets/javascripts/lib/utils/text_utility.js
app/assets/javascripts/lib/utils/text_utility.js
+14
-0
app/assets/javascripts/merge_request_tabs.js
app/assets/javascripts/merge_request_tabs.js
+4
-0
app/assets/javascripts/mr_notes/index.js
app/assets/javascripts/mr_notes/index.js
+41
-0
app/assets/javascripts/notes.js
app/assets/javascripts/notes.js
+84
-54
app/assets/javascripts/notes/components/comment_form.vue
app/assets/javascripts/notes/components/comment_form.vue
+54
-23
app/assets/javascripts/notes/components/diff_file_header.vue
app/assets/javascripts/notes/components/diff_file_header.vue
+92
-0
app/assets/javascripts/notes/components/diff_with_note.vue
app/assets/javascripts/notes/components/diff_with_note.vue
+96
-0
app/assets/javascripts/notes/components/discussion_counter.vue
...ssets/javascripts/notes/components/discussion_counter.vue
+119
-0
app/assets/javascripts/notes/components/note_actions.vue
app/assets/javascripts/notes/components/note_actions.vue
+61
-0
app/assets/javascripts/notes/components/note_body.vue
app/assets/javascripts/notes/components/note_body.vue
+2
-2
app/assets/javascripts/notes/components/note_form.vue
app/assets/javascripts/notes/components/note_form.vue
+31
-12
app/assets/javascripts/notes/components/note_header.vue
app/assets/javascripts/notes/components/note_header.vue
+6
-7
app/assets/javascripts/notes/components/noteable_discussion.vue
...sets/javascripts/notes/components/noteable_discussion.vue
+156
-42
app/assets/javascripts/notes/components/noteable_note.vue
app/assets/javascripts/notes/components/noteable_note.vue
+15
-2
app/assets/javascripts/notes/components/notes_app.vue
app/assets/javascripts/notes/components/notes_app.vue
+34
-9
app/assets/javascripts/notes/constants.js
app/assets/javascripts/notes/constants.js
+5
-1
app/assets/javascripts/notes/index.js
app/assets/javascripts/notes/index.js
+1
-11
app/assets/javascripts/notes/mixins/autosave.js
app/assets/javascripts/notes/mixins/autosave.js
+3
-2
app/assets/javascripts/notes/mixins/noteable.js
app/assets/javascripts/notes/mixins/noteable.js
+22
-0
app/assets/javascripts/notes/mixins/resolvable.js
app/assets/javascripts/notes/mixins/resolvable.js
+50
-0
app/assets/javascripts/notes/services/notes_service.js
app/assets/javascripts/notes/services/notes_service.js
+7
-0
app/assets/javascripts/notes/stores/actions.js
app/assets/javascripts/notes/stores/actions.js
+13
-4
app/assets/javascripts/notes/stores/getters.js
app/assets/javascripts/notes/stores/getters.js
+35
-1
app/assets/javascripts/notes/stores/mutation_types.js
app/assets/javascripts/notes/stores/mutation_types.js
+1
-0
app/assets/javascripts/notes/stores/mutations.js
app/assets/javascripts/notes/stores/mutations.js
+40
-3
app/assets/javascripts/notes/stores/utils.js
app/assets/javascripts/notes/stores/utils.js
+0
-1
app/assets/javascripts/pages/projects/merge_requests/show/index.js
...s/javascripts/pages/projects/merge_requests/show/index.js
+0
-1
app/assets/javascripts/vue_shared/components/clipboard_button.vue
...ts/javascripts/vue_shared/components/clipboard_button.vue
+1
-1
app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
...javascripts/vue_shared/components/issue/issue_warning.vue
+0
-3
app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
...javascripts/vue_shared/components/notes/skeleton_note.vue
+24
-0
app/assets/stylesheets/pages/notes.scss
app/assets/stylesheets/pages/notes.scss
+1
-1
app/controllers/concerns/issuable_actions.rb
app/controllers/concerns/issuable_actions.rb
+14
-0
app/controllers/concerns/notes_actions.rb
app/controllers/concerns/notes_actions.rb
+12
-2
app/controllers/projects/discussions_controller.rb
app/controllers/projects/discussions_controller.rb
+32
-6
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+0
-14
app/controllers/projects/notes_controller.rb
app/controllers/projects/notes_controller.rb
+23
-7
app/helpers/notes_helper.rb
app/helpers/notes_helper.rb
+31
-0
app/models/note.rb
app/models/note.rb
+1
-0
app/serializers/diff_file_entity.rb
app/serializers/diff_file_entity.rb
+41
-0
app/serializers/discussion_entity.rb
app/serializers/discussion_entity.rb
+38
-0
app/serializers/merge_request_widget_entity.rb
app/serializers/merge_request_widget_entity.rb
+12
-0
app/serializers/note_entity.rb
app/serializers/note_entity.rb
+12
-0
app/views/projects/issues/_discussion.html.haml
app/views/projects/issues/_discussion.html.haml
+3
-11
app/views/projects/issues/show.html.haml
app/views/projects/issues/show.html.haml
+1
-1
app/views/projects/merge_requests/show.html.haml
app/views/projects/merge_requests/show.html.haml
+30
-17
app/views/shared/notes/_notes_with_form.html.haml
app/views/shared/notes/_notes_with_form.html.haml
+4
-3
config/routes/project.rb
config/routes/project.rb
+1
-0
config/webpack.config.js
config/webpack.config.js
+2
-0
spec/controllers/projects/discussions_controller_spec.rb
spec/controllers/projects/discussions_controller_spec.rb
+26
-0
spec/controllers/projects/issues_controller_spec.rb
spec/controllers/projects/issues_controller_spec.rb
+1
-1
spec/features/merge_request/user_posts_notes_spec.rb
spec/features/merge_request/user_posts_notes_spec.rb
+1
-1
spec/fixtures/api/schemas/entities/merge_request_widget.json
spec/fixtures/api/schemas/entities/merge_request_widget.json
+4
-1
spec/javascripts/autosave_spec.js
spec/javascripts/autosave_spec.js
+39
-16
spec/javascripts/fixtures/merge_requests.rb
spec/javascripts/fixtures/merge_requests.rb
+42
-0
spec/javascripts/notes/components/comment_form_spec.js
spec/javascripts/notes/components/comment_form_spec.js
+20
-3
spec/javascripts/notes/components/diff_file_header_spec.js
spec/javascripts/notes/components/diff_file_header_spec.js
+93
-0
spec/javascripts/notes/components/diff_with_note_spec.js
spec/javascripts/notes/components/diff_with_note_spec.js
+64
-0
spec/javascripts/notes/components/note_app_spec.js
spec/javascripts/notes/components/note_app_spec.js
+3
-2
spec/javascripts/notes/components/note_body_spec.js
spec/javascripts/notes/components/note_body_spec.js
+18
-9
spec/javascripts/notes/components/note_header_spec.js
spec/javascripts/notes/components/note_header_spec.js
+26
-6
spec/javascripts/notes/mock_data.js
spec/javascripts/notes/mock_data.js
+3
-2
spec/javascripts/notes/stores/getters_spec.js
spec/javascripts/notes/stores/getters_spec.js
+2
-2
spec/javascripts/notes/stores/mutation_spec.js
spec/javascripts/notes/stores/mutation_spec.js
+3
-2
spec/javascripts/notes_spec.js
spec/javascripts/notes_spec.js
+6
-5
spec/serializers/diff_file_entity_spec.rb
spec/serializers/diff_file_entity_spec.rb
+24
-0
spec/serializers/discussion_entity_spec.rb
spec/serializers/discussion_entity_spec.rb
+36
-0
spec/serializers/note_entity_spec.rb
spec/serializers/note_entity_spec.rb
+11
-0
No files found.
app/assets/javascripts/autosave.js
View file @
059ab73b
...
...
@@ -3,10 +3,10 @@
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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
import
Vue
from
'
vue
'
;
import
notesApp
from
'
../notes/components/notes_app.vue
'
;
import
discussionCounter
from
'
../notes/components/discussion_counter.vue
'
;
import
store
from
'
../notes/stores
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
{
new
Vue
({
// eslint-disable-line
el
:
'
#js-vue-mr-discussions
'
,
components
:
{
notesApp
,
},
data
()
{
const
notesDataset
=
document
.
getElementById
(
'
js-vue-mr-discussions
'
).
dataset
;
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
:
JSON
.
parse
(
notesDataset
.
currentUserData
),
notesData
:
JSON
.
parse
(
notesDataset
.
notesData
),
};
},
render
(
createElement
)
{
return
createElement
(
'
notes-app
'
,
{
props
:
{
noteableData
:
this
.
noteableData
,
notesData
:
this
.
notesData
,
userData
:
this
.
currentUserData
,
},
});
},
});
new
Vue
({
// eslint-disable-line
el
:
'
#js-vue-discussion-counter
'
,
components
:
{
discussionCounter
,
},
store
,
render
(
createElement
)
{
return
createElement
(
'
discussion-counter
'
);
},
});
});
app/assets/javascripts/notes.js
View file @
059ab73b
This diff is collapsed.
Click to expand it.
app/assets/javascripts/notes/components/comment_form.vue
View file @
059ab73b
...
...
@@ -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 @
059ab73b
<
script
>
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
components
:
{
ClipboardButton
,
Icon
,
},
props
:
{
diffFile
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
titleTag
()
{
return
this
.
diffFile
.
discussionPath
?
'
a
'
:
'
span
'
;
},
},
};
</
script
>
<
template
>
<div
class=
"file-header-content"
>
<div
v-if=
"diffFile.submodule"
>
<span>
<icon
name=
"archive"
/>
<strong
v-html=
"diffFile.submoduleLink"
class=
"file-title-name"
></strong>
<clipboard-button
title=
"Copy file path to clipboard"
:text=
"diffFile.submoduleLink"
/>
</span>
</div>
<template
v-else
>
<component
ref=
"titleWrapper"
:is=
"titleTag"
:href=
"diffFile.discussionPath"
>
<span
v-html=
"diffFile.blobIcon"
></span>
<span
v-if=
"diffFile.renamedFile"
>
<strong
class=
"file-title-name has-tooltip"
:title=
"diffFile.oldPath"
data-container=
"body"
>
{{
diffFile
.
oldPath
}}
</strong>
→
<strong
class=
"file-title-name has-tooltip"
:title=
"diffFile.newPath"
data-container=
"body"
>
{{
diffFile
.
newPath
}}
</strong>
</span>
<strong
v-else
class=
"file-title-name has-tooltip"
:title=
"diffFile.oldPath"
data-container=
"body"
>
{{
diffFile
.
filePath
}}
<span
v-if=
"diffFile.deletedFile"
>
deleted
</span>
</strong>
</component>
<clipboard-button
title=
"Copy file path to clipboard"
:text=
"diffFile.filePath"
/>
<small
v-if=
"diffFile.modeChanged"
ref=
"fileMode"
>
{{
diffFile
.
aMode
}}
→
{{
diffFile
.
bMode
}}
</small>
</
template
>
</div>
</template>
app/assets/javascripts/notes/components/diff_with_note.vue
0 → 100644
View file @
059ab73b
<
script
>
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
imageDiffHelper
from
'
~/image_diff/helpers/index
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
DiffFileHeader
from
'
./diff_file_header.vue
'
;
export
default
{
components
:
{
DiffFileHeader
,
},
props
:
{
discussion
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
isImageDiff
()
{
return
!
this
.
diffFile
.
text
;
},
diffFileClass
()
{
const
{
text
}
=
this
.
diffFile
;
return
text
?
'
text-file
'
:
'
js-image-file
'
;
},
diffRows
()
{
return
$
(
this
.
discussion
.
truncatedDiffLines
);
},
diffFile
()
{
return
convertObjectPropsToCamelCase
(
this
.
discussion
.
diffFile
);
},
imageDiffHtml
()
{
return
this
.
discussion
.
imageDiffHtml
;
},
},
mounted
()
{
if
(
this
.
isImageDiff
)
{
const
canCreateNote
=
false
;
const
renderCommentBadge
=
true
;
imageDiffHelper
.
initImageDiff
(
this
.
$refs
.
fileHolder
,
canCreateNote
,
renderCommentBadge
);
}
else
{
const
fileHolder
=
$
(
this
.
$refs
.
fileHolder
);
this
.
$nextTick
(()
=>
{
syntaxHighlight
(
fileHolder
);
});
}
},
methods
:
{
rowTag
(
html
)
{
return
html
.
outerHTML
?
'
tr
'
:
'
template
'
;
},
},
};
</
script
>
<
template
>
<div
ref=
"fileHolder"
class=
"diff-file file-holder"
:class=
"diffFileClass"
>
<div
class=
"js-file-title file-title file-title-flex-parent"
>
<diff-file-header
:diff-file=
"diffFile"
/>
</div>
<div
v-if=
"diffFile.text"
class=
"diff-content code js-syntax-highlight"
>
<table>
<component
:is=
"rowTag(html)"
:class=
"html.className"
v-for=
"(html, index) in diffRows"
v-html=
"html.outerHTML"
:key=
"index"
/>
<tr
class=
"notes_holder"
>
<td
class=
"notes_line"
colspan=
"2"
></td>
<td
class=
"notes_content"
>
<slot></slot>
</td>
</tr>
</table>
</div>
<div
v-else
>
<div
v-html=
"imageDiffHtml"
></div>
<slot></slot>
</div>
</div>
</
template
>
app/assets/javascripts/notes/components/discussion_counter.vue
0 → 100644
View file @
059ab73b
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
resolveSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
mrIssueSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionSvg
from
'
icons/_next_discussion.svg
'
;
import
{
pluralize
}
from
'
../../lib/utils/text_utility
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
export
default
{
directives
:
{
tooltip
,
},
computed
:
{
...
mapGetters
([
'
getUserData
'
,
'
getNoteableData
'
,
'
discussionCount
'
,
'
unresolvedDiscussions
'
,
'
resolvedDiscussionCount
'
,
]),
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
hasNextButton
()
{
return
this
.
isLoggedIn
&&
!
this
.
allResolved
;
},
countText
()
{
return
pluralize
(
'
discussion
'
,
this
.
discussionCount
);
},
allResolved
()
{
return
this
.
resolvedDiscussionCount
===
this
.
discussionCount
;
},
resolveAllDiscussionsIssuePath
()
{
return
this
.
getNoteableData
.
create_issue_to_resolve_discussions_path
;
},
firstUnresolvedDiscussionId
()
{
const
item
=
this
.
unresolvedDiscussions
[
0
]
||
{};
return
item
.
id
;
},
},
created
()
{
this
.
resolveSvg
=
resolveSvg
;
this
.
resolvedSvg
=
resolvedSvg
;
this
.
mrIssueSvg
=
mrIssueSvg
;
this
.
nextDiscussionSvg
=
nextDiscussionSvg
;
},
methods
:
{
jumpToFirstDiscussion
()
{
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
this
.
firstUnresolvedDiscussionId
}
"]`
);
const
activeTab
=
window
.
mrTabs
.
currentAction
;
if
(
activeTab
===
'
commits
'
||
activeTab
===
'
pipelines
'
)
{
window
.
mrTabs
.
activateTab
(
'
show
'
);
}
if
(
el
)
{
scrollToElement
(
el
);
}
},
},
};
</
script
>
<
template
>
<div
class=
"line-resolve-all-container prepend-top-10"
>
<div>
<div
v-if=
"discussionCount > 0"
:class=
"
{ 'has-next-btn': hasNextButton }"
class="line-resolve-all">
<span
:class=
"
{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
type="button">
<span
v-if=
"allResolved"
v-html=
"resolvedSvg"
></span>
<span
v-else
v-html=
"resolveSvg"
></span>
</span>
<span
class=
".line-resolve-text"
>
{{
resolvedDiscussionCount
}}
/
{{
discussionCount
}}
{{
countText
}}
resolved
</span>
</div>
<div
v-if=
"resolveAllDiscussionsIssuePath && !allResolved"
class=
"btn-group"
role=
"group"
>
<a
:href=
"resolveAllDiscussionsIssuePath"
v-tooltip
title=
"Resolve all discussions in new issue"
data-container=
"body"
class=
"new-issue-for-discussion btn btn-default discussion-create-issue-btn"
>
<span
v-html=
"mrIssueSvg"
></span>
</a>
</div>
<div
v-if=
"isLoggedIn && !allResolved"
class=
"btn-group"
role=
"group"
>
<button
@
click=
"jumpToFirstDiscussion"
v-tooltip
title=
"Jump to first unresolved discussion"
data-container=
"body"
class=
"btn btn-default discussion-next-btn"
>
<span
v-html=
"nextDiscussionSvg"
></span>
</button>
</div>
</div>
</div>
</
template
>
app/assets/javascripts/notes/components/note_actions.vue
View file @
059ab73b
...
...
@@ -4,6 +4,8 @@
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 @
059ab73b
...
...
@@ -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 @
059ab73b
<
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 @
059ab73b
...
...
@@ -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 @
059ab73b
This diff is collapsed.
Click to expand it.
app/assets/javascripts/notes/components/noteable_note.vue
View file @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
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 @
059ab73b
...
...
@@ -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 @
059ab73b
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 @
059ab73b
import
*
as
constants
from
'
../constants
'
;
export
default
{
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
noteableType
()
{
switch
(
this
.
note
.
noteable_type
)
{
case
'
MergeRequest
'
:
return
constants
.
MERGE_REQUEST_NOTEABLE_TYPE
;
case
'
Issue
'
:
return
constants
.
ISSUE_NOTEABLE_TYPE
;
default
:
return
''
;
}
},
},
};
app/assets/javascripts/notes/mixins/resolvable.js
0 → 100644
View file @
059ab73b
import
Flash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
export
default
{
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
discussionResolved
()
{
const
{
notes
,
resolved
}
=
this
.
note
;
if
(
notes
)
{
// Decide resolved state using store. Only valid for discussions.
return
notes
.
every
(
note
=>
note
.
resolved
&&
!
note
.
system
);
}
return
resolved
;
},
resolveButtonTitle
()
{
if
(
this
.
updatedNoteBody
)
{
if
(
this
.
discussionResolved
)
{
return
__
(
'
Comment and unresolve discussion
'
);
}
return
__
(
'
Comment and resolve discussion
'
);
}
return
this
.
discussionResolved
?
__
(
'
Unresolve discussion
'
)
:
__
(
'
Resolve discussion
'
);
},
},
methods
:
{
resolveHandler
(
resolvedState
=
false
)
{
this
.
isResolving
=
true
;
const
endpoint
=
this
.
note
.
resolve_path
||
`
${
this
.
note
.
path
}
/resolve`
;
const
isResolved
=
this
.
discussionResolved
||
resolvedState
;
const
discussion
=
this
.
resolveAsThread
;
this
.
toggleResolveNote
({
endpoint
,
isResolved
,
discussion
})
.
then
(()
=>
{
this
.
isResolving
=
false
;
})
.
catch
(()
=>
{
this
.
isResolving
=
false
;
const
msg
=
__
(
'
Something went wrong while resolving this discussion. Please try again.
'
);
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
});
},
},
};
app/assets/javascripts/notes/services/notes_service.js
View file @
059ab73b
import
Vue
from
'
vue
'
;
import
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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -22,7 +22,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 @
059ab73b
<
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 @
059ab73b
...
...
@@ -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 @
059ab73b
<
template
>
<li
class=
"timeline-entry note"
>
<div
class=
"timeline-entry-inner"
>
<div
class=
"timeline-icon"
>
</div>
<div
class=
"timeline-content"
>
<div
class=
"note-header"
></div>
<div
class=
"note-body"
>
<skeleton-loading-container
/>
</div>
</div>
</div>
</li>
</
template
>
<
script
>
import
skeletonLoadingContainer
from
'
~/vue_shared/components/skeleton_loading_container.vue
'
;
export
default
{
components
:
{
skeletonLoadingContainer
,
},
};
</
script
>
app/assets/stylesheets/pages/notes.scss
View file @
059ab73b
...
...
@@ -723,7 +723,7 @@ ul.notes {
.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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
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 @
059ab73b
...
...
@@ -60,20 +60,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 @
059ab73b
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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -133,6 +133,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 @
059ab73b
class
DiffFileEntity
<
Grape
::
Entity
include
DiffHelper
include
SubmoduleHelper
include
BlobHelper
include
IconsHelper
include
ActionView
::
Helpers
::
TagHelper
expose
:submodule?
,
as: :submodule
expose
:submodule_link
do
|
diff_file
|
submodule_links
(
diff_file
.
blob
,
diff_file
.
content_sha
,
diff_file
.
repository
).
first
end
expose
:blob_path
do
|
diff_file
|
diff_file
.
blob
.
path
end
expose
:blob_icon
do
|
diff_file
|
blob_icon
(
diff_file
.
b_mode
,
diff_file
.
file_path
)
end
expose
:file_path
expose
:deleted_file?
,
as: :deleted_file
expose
:renamed_file?
,
as: :renamed_file
expose
:old_path
expose
:new_path
expose
:mode_changed?
,
as: :mode_changed
expose
:a_mode
expose
:b_mode
expose
:text?
,
as: :text
expose
:old_path_html
do
|
diff_file
|
old_path
=
mark_inline_diffs
(
diff_file
.
old_path
,
diff_file
.
new_path
)
old_path
end
expose
:new_path_html
do
|
diff_file
|
_
,
new_path
=
mark_inline_diffs
(
diff_file
.
old_path
,
diff_file
.
new_path
)
new_path
end
end
app/serializers/discussion_entity.rb
View file @
059ab73b
...
...
@@ -7,4 +7,42 @@ class DiscussionEntity < Grape::Entity
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 @
059ab73b
...
...
@@ -115,6 +115,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
...
...
@@ -189,6 +197,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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -73,7 +73,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 @
059ab73b
-
@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
...
...
@@ -7,6 +8,9 @@
-
content_for
:page_specific_javascripts
do
=
webpack_bundle_tag
(
'common_vue'
)
-
if
has_vue_discussions_cookie?
=
webpack_bundle_tag
(
'mr_notes'
)
.merge-request
{
data:
{
mr_action:
j
(
params
[
:tab
].
presence
||
'show'
),
url:
merge_request_path
(
@merge_request
,
format: :json
),
project_path:
project_path
(
@merge_request
.
project
)
}
}
=
render
"projects/merge_requests/mr_title"
...
...
@@ -23,7 +27,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'
)
}
...
...
@@ -51,28 +55,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 @
059ab73b
-
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 @
059ab73b
...
...
@@ -103,6 +103,7 @@ constraints(ProjectUrlConstrainer.new) do
post
:toggle_subscription
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 @
059ab73b
...
...
@@ -52,6 +52,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
'
,
...
...
@@ -243,6 +244,7 @@ var config = {
'
groups
'
,
'
merge_conflicts
'
,
'
monitoring
'
,
'
mr_notes
'
,
'
notebook_viewer
'
,
'
pdf_viewer
'
,
'
pipelines
'
,
...
...
spec/controllers/projects/discussions_controller_spec.rb
View file @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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
context
'with cross-reference system note'
,
:request_store
do
...
...
spec/features/merge_request/user_posts_notes_spec.rb
View file @
059ab73b
...
...
@@ -144,7 +144,7 @@ describe 'Merge request > User posts notes', :js do
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 @
059ab73b
...
...
@@ -75,7 +75,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
},
...
...
@@ -103,6 +105,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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
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 @
059ab73b
import
Vue
from
'
vue
'
;
import
DiffFileHeader
from
'
~/notes/components/diff_file_header.vue
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
discussionFixture
=
'
merge_requests/diff_discussion.json
'
;
describe
(
'
diff_file_header
'
,
()
=>
{
let
vm
;
const
diffDiscussionMock
=
getJSONFixture
(
discussionFixture
)[
0
];
const
diffFile
=
convertObjectPropsToCamelCase
(
diffDiscussionMock
.
diff_file
);
const
props
=
{
diffFile
,
};
const
Component
=
Vue
.
extend
(
DiffFileHeader
);
const
selectors
=
{
get
copyButton
()
{
return
vm
.
$el
.
querySelector
(
'
button[data-original-title="Copy file path to clipboard"]
'
);
},
get
fileName
()
{
return
vm
.
$el
.
querySelector
(
'
.file-title-name
'
);
},
get
titleWrapper
()
{
return
vm
.
$refs
.
titleWrapper
;
},
};
describe
(
'
submodule
'
,
()
=>
{
beforeEach
(()
=>
{
props
.
diffFile
.
submodule
=
true
;
props
.
diffFile
.
submoduleLink
=
'
<a href="/bha">Submodule</a>
'
;
vm
=
mountComponent
(
Component
,
props
);
});
it
(
'
shows submoduleLink
'
,
()
=>
{
expect
(
selectors
.
fileName
.
innerHTML
).
toBe
(
props
.
diffFile
.
submoduleLink
);
});
it
(
'
has button to copy blob path
'
,
()
=>
{
expect
(
selectors
.
copyButton
).
toExist
();
expect
(
selectors
.
copyButton
.
getAttribute
(
'
data-clipboard-text
'
)).
toBe
(
props
.
diffFile
.
submoduleLink
);
});
});
describe
(
'
changed file
'
,
()
=>
{
beforeEach
(()
=>
{
props
.
diffFile
.
submodule
=
false
;
props
.
diffFile
.
discussionPath
=
'
some/discussion/id
'
;
vm
=
mountComponent
(
Component
,
props
);
});
it
(
'
shows file type icon
'
,
()
=>
{
expect
(
vm
.
$el
.
innerHTML
).
toContain
(
'
fa-file-text-o
'
);
});
it
(
'
links to discussion path
'
,
()
=>
{
expect
(
selectors
.
titleWrapper
).
toExist
();
expect
(
selectors
.
titleWrapper
.
tagName
).
toBe
(
'
A
'
);
expect
(
selectors
.
titleWrapper
.
getAttribute
(
'
href
'
)).
toBe
(
props
.
diffFile
.
discussionPath
);
});
it
(
'
shows plain title if no link given
'
,
()
=>
{
props
.
diffFile
.
discussionPath
=
undefined
;
vm
=
mountComponent
(
Component
,
props
);
expect
(
selectors
.
titleWrapper
.
tagName
).
not
.
toBe
(
'
A
'
);
expect
(
selectors
.
titleWrapper
.
href
).
toBeFalsy
();
});
it
(
'
has button to copy file path
'
,
()
=>
{
expect
(
selectors
.
copyButton
).
toExist
();
expect
(
selectors
.
copyButton
.
getAttribute
(
'
data-clipboard-text
'
)).
toBe
(
props
.
diffFile
.
filePath
);
});
it
(
'
shows file mode change
'
,
(
done
)
=>
{
vm
.
diffFile
=
{
...
props
.
diffFile
,
modeChanged
:
true
,
aMode
:
'
100755
'
,
bMode
:
'
100644
'
,
};
Vue
.
nextTick
(()
=>
{
expect
(
vm
.
$refs
.
fileMode
.
textContent
.
trim
(),
).
toBe
(
'
100755 → 100644
'
);
done
();
});
});
});
});
spec/javascripts/notes/components/diff_with_note_spec.js
0 → 100644
View file @
059ab73b
import
Vue
from
'
vue
'
;
import
DiffWithNote
from
'
~/notes/components/diff_with_note.vue
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
mountComponent
from
'
../../helpers/vue_mount_component_helper
'
;
const
discussionFixture
=
'
merge_requests/diff_discussion.json
'
;
const
imageDiscussionFixture
=
'
merge_requests/image_diff_discussion.json
'
;
describe
(
'
diff_with_note
'
,
()
=>
{
let
vm
;
const
diffDiscussionMock
=
getJSONFixture
(
discussionFixture
)[
0
];
const
diffDiscussion
=
convertObjectPropsToCamelCase
(
diffDiscussionMock
);
const
Component
=
Vue
.
extend
(
DiffWithNote
);
const
props
=
{
discussion
:
diffDiscussion
,
};
const
selectors
=
{
get
container
()
{
return
vm
.
$refs
.
fileHolder
;
},
get
diffTable
()
{
return
this
.
container
.
querySelector
(
'
.diff-content table
'
);
},
get
diffRows
()
{
return
this
.
container
.
querySelectorAll
(
'
.diff-content .line_holder
'
);
},
get
noteRow
()
{
return
this
.
container
.
querySelector
(
'
.diff-content .notes_holder
'
);
},
};
describe
(
'
text diff
'
,
()
=>
{
beforeEach
(()
=>
{
vm
=
mountComponent
(
Component
,
props
);
});
it
(
'
shows text diff
'
,
()
=>
{
expect
(
selectors
.
container
).
toHaveClass
(
'
text-file
'
);
expect
(
selectors
.
diffTable
).
toExist
();
});
it
(
'
shows diff lines
'
,
()
=>
{
expect
(
selectors
.
diffRows
.
length
).
toBe
(
12
);
});
it
(
'
shows notes row
'
,
()
=>
{
expect
(
selectors
.
noteRow
).
toExist
();
});
});
describe
(
'
image diff
'
,
()
=>
{
beforeEach
(()
=>
{
const
imageDiffDiscussionMock
=
getJSONFixture
(
imageDiscussionFixture
)[
0
];
props
.
discussion
=
convertObjectPropsToCamelCase
(
imageDiffDiscussionMock
);
});
it
(
'
shows image diff
'
,
()
=>
{
vm
=
mountComponent
(
Component
,
props
);
expect
(
selectors
.
container
).
toHaveClass
(
'
js-image-file
'
);
expect
(
selectors
.
diffTable
).
not
.
toExist
();
});
});
});
spec/javascripts/notes/components/note_app_spec.js
View file @
059ab73b
...
...
@@ -24,6 +24,7 @@ describe('note_app', () => {
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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
...
...
@@ -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 @
059ab73b
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 @
059ab73b
...
...
@@ -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 @
059ab73b
require
'spec_helper'
describe
DiffFileEntity
do
include
RepoHelpers
let
(
:project
)
{
create
(
:project
,
:repository
)
}
let
(
:repository
)
{
project
.
repository
}
let
(
:commit
)
{
project
.
commit
(
sample_commit
.
id
)
}
let
(
:diff_refs
)
{
commit
.
diff_refs
}
let
(
:diff
)
{
commit
.
raw_diffs
.
first
}
let
(
:diff_file
)
{
Gitlab
::
Diff
::
File
.
new
(
diff
,
diff_refs:
diff_refs
,
repository:
repository
)
}
let
(
:entity
)
{
described_class
.
new
(
diff_file
)
}
subject
{
entity
.
as_json
}
it
'exposes correct attributes'
do
expect
(
subject
).
to
include
(
:submodule
,
:submodule_link
,
:file_path
,
:deleted_file
,
:old_path
,
:new_path
,
:mode_changed
,
:a_mode
,
:b_mode
,
:text
,
:old_path_html
,
:new_path_html
)
end
end
spec/serializers/discussion_entity_spec.rb
0 → 100644
View file @
059ab73b
require
'spec_helper'
describe
DiscussionEntity
do
include
RepoHelpers
let
(
:user
)
{
create
(
:user
)
}
let
(
:note
)
{
create
(
:discussion_note_on_merge_request
)
}
let
(
:discussion
)
{
note
.
discussion
}
let
(
:request
)
{
double
(
'request'
)
}
let
(
:controller
)
{
double
(
'controller'
)
}
let
(
:entity
)
{
described_class
.
new
(
discussion
,
request:
request
,
context:
controller
)
}
subject
{
entity
.
as_json
}
before
do
allow
(
controller
).
to
receive
(
:render_to_string
)
allow
(
request
).
to
receive
(
:current_user
).
and_return
(
user
)
allow
(
request
).
to
receive
(
:noteable
).
and_return
(
note
.
noteable
)
end
it
'exposes correct attributes'
do
expect
(
subject
).
to
include
(
:id
,
:expanded
,
:notes
,
:individual_note
,
:resolvable
,
:resolved
,
:resolve_path
,
:resolve_with_issue_path
,
:diff_discussion
)
end
context
'when diff file is present'
do
let
(
:note
)
{
create
(
:diff_note_on_merge_request
)
}
it
'exposes diff file attributes'
do
expect
(
subject
).
to
include
(
:diff_file
,
:truncated_diff_lines
,
:image_diff_html
)
end
end
end
spec/serializers/note_entity_spec.rb
View file @
059ab73b
...
...
@@ -48,4 +48,15 @@ describe NoteEntity do
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