Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Boxiang Sun
gitlab-ce
Commits
fdc9ae2e
Commit
fdc9ae2e
authored
Mar 16, 2018
by
Fatih Acet
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Prettify notes.
parent
2c792c75
Changes
26
Hide whitespace changes
Inline
Side-by-side
Showing
26 changed files
with
2149 additions
and
1792 deletions
+2149
-1792
app/assets/javascripts/mr_notes/index.js
app/assets/javascripts/mr_notes/index.js
+6
-3
app/assets/javascripts/notes.js
app/assets/javascripts/notes.js
+460
-204
app/assets/javascripts/notes/components/comment_form.vue
app/assets/javascripts/notes/components/comment_form.vue
+279
-273
app/assets/javascripts/notes/components/diff_file_header.vue
app/assets/javascripts/notes/components/diff_file_header.vue
+17
-17
app/assets/javascripts/notes/components/diff_with_note.vue
app/assets/javascripts/notes/components/diff_with_note.vue
+50
-46
app/assets/javascripts/notes/components/discussion_counter.vue
...ssets/javascripts/notes/components/discussion_counter.vue
+58
-56
app/assets/javascripts/notes/components/discussion_locked_widget.vue
...javascripts/notes/components/discussion_locked_widget.vue
+8
-10
app/assets/javascripts/notes/components/note_actions.vue
app/assets/javascripts/notes/components/note_actions.vue
+112
-114
app/assets/javascripts/notes/components/note_attachment.vue
app/assets/javascripts/notes/components/note_attachment.vue
+8
-8
app/assets/javascripts/notes/components/note_awards_list.vue
app/assets/javascripts/notes/components/note_awards_list.vue
+181
-168
app/assets/javascripts/notes/components/note_body.vue
app/assets/javascripts/notes/components/note_body.vue
+66
-68
app/assets/javascripts/notes/components/note_edited_text.vue
app/assets/javascripts/notes/components/note_edited_text.vue
+25
-25
app/assets/javascripts/notes/components/note_form.vue
app/assets/javascripts/notes/components/note_form.vue
+116
-108
app/assets/javascripts/notes/components/note_header.vue
app/assets/javascripts/notes/components/note_header.vue
+54
-56
app/assets/javascripts/notes/components/note_signed_out_widget.vue
...s/javascripts/notes/components/note_signed_out_widget.vue
+11
-13
app/assets/javascripts/notes/components/noteable_discussion.vue
...sets/javascripts/notes/components/noteable_discussion.vue
+180
-180
app/assets/javascripts/notes/components/noteable_note.vue
app/assets/javascripts/notes/components/noteable_note.vue
+130
-130
app/assets/javascripts/notes/components/notes_app.vue
app/assets/javascripts/notes/components/notes_app.vue
+142
-140
app/assets/javascripts/notes/index.js
app/assets/javascripts/notes/index.js
+38
-30
app/assets/javascripts/notes/mixins/autosave.js
app/assets/javascripts/notes/mixins/autosave.js
+5
-1
app/assets/javascripts/notes/mixins/resolvable.js
app/assets/javascripts/notes/mixins/resolvable.js
+8
-3
app/assets/javascripts/notes/services/notes_service.js
app/assets/javascripts/notes/services/notes_service.js
+3
-1
app/assets/javascripts/notes/stores/actions.js
app/assets/javascripts/notes/stores/actions.js
+147
-108
app/assets/javascripts/notes/stores/getters.js
app/assets/javascripts/notes/stores/getters.js
+19
-15
app/assets/javascripts/notes/stores/mutations.js
app/assets/javascripts/notes/stores/mutations.js
+18
-10
app/assets/javascripts/notes/stores/utils.js
app/assets/javascripts/notes/stores/utils.js
+8
-5
No files found.
app/assets/javascripts/mr_notes/index.js
View file @
fdc9ae2e
...
@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
...
@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import
store
from
'
../notes/stores
'
;
import
store
from
'
../notes/stores
'
;
export
default
function
initMrNotes
()
{
export
default
function
initMrNotes
()
{
new
Vue
({
// eslint-disable-line
// eslint-disable-next-line no-new
new
Vue
({
el
:
'
#js-vue-mr-discussions
'
,
el
:
'
#js-vue-mr-discussions
'
,
components
:
{
components
:
{
notesApp
,
notesApp
,
},
},
data
()
{
data
()
{
const
notesDataset
=
document
.
getElementById
(
'
js-vue-mr-discussions
'
).
dataset
;
const
notesDataset
=
document
.
getElementById
(
'
js-vue-mr-discussions
'
)
.
dataset
;
return
{
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
:
JSON
.
parse
(
notesDataset
.
currentUserData
),
currentUserData
:
JSON
.
parse
(
notesDataset
.
currentUserData
),
...
@@ -28,7 +30,8 @@ export default function initMrNotes() {
...
@@ -28,7 +30,8 @@ export default function initMrNotes() {
},
},
});
});
new
Vue
({
// eslint-disable-line
// eslint-disable-next-line no-new
new
Vue
({
el
:
'
#js-vue-discussion-counter
'
,
el
:
'
#js-vue-discussion-counter
'
,
components
:
{
components
:
{
discussionCounter
,
discussionCounter
,
...
...
app/assets/javascripts/notes.js
View file @
fdc9ae2e
...
@@ -28,7 +28,13 @@ import GLForm from './gl_form';
...
@@ -28,7 +28,13 @@ import GLForm from './gl_form';
import
loadAwardsHandler
from
'
./awards_handler
'
;
import
loadAwardsHandler
from
'
./awards_handler
'
;
import
Autosave
from
'
./autosave
'
;
import
Autosave
from
'
./autosave
'
;
import
TaskList
from
'
./task_list
'
;
import
TaskList
from
'
./task_list
'
;
import
{
isInViewport
,
getPagePath
,
scrollToElement
,
isMetaKey
,
hasVueMRDiscussionsCookie
}
from
'
./lib/utils/common_utils
'
;
import
{
isInViewport
,
getPagePath
,
scrollToElement
,
isMetaKey
,
hasVueMRDiscussionsCookie
,
}
from
'
./lib/utils/common_utils
'
;
import
imageDiffHelper
from
'
./image_diff/helpers/index
'
;
import
imageDiffHelper
from
'
./image_diff/helpers/index
'
;
import
{
localTimeAgo
}
from
'
./lib/utils/datetime_utility
'
;
import
{
localTimeAgo
}
from
'
./lib/utils/datetime_utility
'
;
...
@@ -42,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
...
@@ -42,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const
REGEX_QUICK_ACTIONS
=
/^
\/\w
+.*$/gm
;
const
REGEX_QUICK_ACTIONS
=
/^
\/\w
+.*$/gm
;
export
default
class
Notes
{
export
default
class
Notes
{
static
initialize
(
notes_url
,
note_ids
,
last_fetched_at
,
view
,
enableGFM
=
true
)
{
static
initialize
(
notes_url
,
note_ids
,
last_fetched_at
,
view
,
enableGFM
=
true
,
)
{
if
(
!
this
.
instance
)
{
if
(
!
this
.
instance
)
{
this
.
instance
=
new
Notes
(
notes_url
,
note_ids
,
last_fetched_at
,
view
,
enableGFM
);
this
.
instance
=
new
Notes
(
notes_url
,
note_ids
,
last_fetched_at
,
view
,
enableGFM
,
);
}
}
}
}
...
@@ -82,7 +100,8 @@ export default class Notes {
...
@@ -82,7 +100,8 @@ export default class Notes {
this
.
updatedNotesTrackingMap
=
{};
this
.
updatedNotesTrackingMap
=
{};
this
.
last_fetched_at
=
last_fetched_at
;
this
.
last_fetched_at
=
last_fetched_at
;
this
.
noteable_url
=
document
.
URL
;
this
.
noteable_url
=
document
.
URL
;
this
.
notesCountBadge
||
(
this
.
notesCountBadge
=
$
(
'
.issuable-details
'
).
find
(
'
.notes-tab .badge
'
));
this
.
notesCountBadge
||
(
this
.
notesCountBadge
=
$
(
'
.issuable-details
'
).
find
(
'
.notes-tab .badge
'
));
this
.
basePollingInterval
=
15000
;
this
.
basePollingInterval
=
15000
;
this
.
maxPollingSteps
=
4
;
this
.
maxPollingSteps
=
4
;
...
@@ -93,15 +112,17 @@ export default class Notes {
...
@@ -93,15 +112,17 @@ export default class Notes {
this
.
taskList
=
new
TaskList
({
this
.
taskList
=
new
TaskList
({
dataType
:
'
note
'
,
dataType
:
'
note
'
,
fieldName
:
'
note
'
,
fieldName
:
'
note
'
,
selector
:
'
.notes
'
selector
:
'
.notes
'
,
});
});
this
.
collapseLongCommitList
();
this
.
collapseLongCommitList
();
this
.
setViewType
(
view
);
this
.
setViewType
(
view
);
// We are in the Merge Requests page so we need another edit form for Changes tab
// We are in the Merge Requests page so we need another edit form for Changes tab
if
(
getPagePath
(
1
)
===
'
merge_requests
'
)
{
if
(
getPagePath
(
1
)
===
'
merge_requests
'
)
{
$
(
'
.note-edit-form
'
).
clone
()
$
(
'
.note-edit-form
'
)
.
addClass
(
'
mr-note-edit-form
'
).
insertAfter
(
'
.note-edit-form
'
);
.
clone
()
.
addClass
(
'
mr-note-edit-form
'
)
.
insertAfter
(
'
.note-edit-form
'
);
}
}
const
hash
=
getLocationHash
();
const
hash
=
getLocationHash
();
...
@@ -117,7 +138,9 @@ export default class Notes {
...
@@ -117,7 +138,9 @@ export default class Notes {
}
}
addBinding
()
{
addBinding
()
{
this
.
$wrapperEl
=
hasVueMRDiscussionsCookie
()
?
$
(
document
).
find
(
'
.diffs
'
)
:
$
(
document
);
this
.
$wrapperEl
=
hasVueMRDiscussionsCookie
()
?
$
(
document
).
find
(
'
.diffs
'
)
:
$
(
document
);
// Edit note link
// Edit note link
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-edit
'
,
this
.
showEditForm
.
bind
(
this
));
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-edit
'
,
this
.
showEditForm
.
bind
(
this
));
...
@@ -125,27 +148,55 @@ export default class Notes {
...
@@ -125,27 +148,55 @@ export default class Notes {
// Reopen and close actions for Issue/MR combined with note form submit
// Reopen and close actions for Issue/MR combined with note form submit
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-submit-button
'
,
this
.
postComment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-submit-button
'
,
this
.
postComment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-save-button
'
,
this
.
updateComment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-save-button
'
,
this
.
updateComment
);
this
.
$wrapperEl
.
on
(
'
keyup input
'
,
'
.js-note-text
'
,
this
.
updateTargetButtons
);
this
.
$wrapperEl
.
on
(
'
keyup input
'
,
'
.js-note-text
'
,
this
.
updateTargetButtons
,
);
// resolve a discussion
// resolve a discussion
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-resolve-button
'
,
this
.
postComment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-resolve-button
'
,
this
.
postComment
);
// remove a note (in general)
// remove a note (in general)
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-delete
'
,
this
.
removeNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-delete
'
,
this
.
removeNote
);
// delete note attachment
// delete note attachment
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-attachment-delete
'
,
this
.
removeAttachment
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-attachment-delete
'
,
this
.
removeAttachment
,
);
// reset main target form when clicking discard
// reset main target form when clicking discard
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-discard
'
,
this
.
resetMainTargetForm
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-discard
'
,
this
.
resetMainTargetForm
);
// update the file name when an attachment is selected
// update the file name when an attachment is selected
this
.
$wrapperEl
.
on
(
'
change
'
,
'
.js-note-attachment-input
'
,
this
.
updateFormAttachment
);
this
.
$wrapperEl
.
on
(
'
change
'
,
'
.js-note-attachment-input
'
,
this
.
updateFormAttachment
,
);
// reply to diff/discussion notes
// reply to diff/discussion notes
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-discussion-reply-button
'
,
this
.
onReplyToDiscussionNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-discussion-reply-button
'
,
this
.
onReplyToDiscussionNote
,
);
// add diff note
// add diff note
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-add-diff-note-button
'
,
this
.
onAddDiffNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-add-diff-note-button
'
,
this
.
onAddDiffNote
);
// add diff note for images
// add diff note for images
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-add-image-diff-note-button
'
,
this
.
onAddImageDiffNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-add-image-diff-note-button
'
,
this
.
onAddImageDiffNote
,
);
// hide diff note form
// hide diff note form
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-close-discussion-note-form
'
,
this
.
cancelDiscussionForm
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-close-discussion-note-form
'
,
this
.
cancelDiscussionForm
,
);
// toggle commit list
// toggle commit list
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.system-note-commit-list-toggler
'
,
this
.
toggleCommitList
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.system-note-commit-list-toggler
'
,
this
.
toggleCommitList
,
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-toggle-lazy-diff
'
,
this
.
loadLazyDiff
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-toggle-lazy-diff
'
,
this
.
loadLazyDiff
);
// fetch notes when tab becomes visible
// fetch notes when tab becomes visible
...
@@ -154,9 +205,21 @@ export default class Notes {
...
@@ -154,9 +205,21 @@ export default class Notes {
this
.
$wrapperEl
.
on
(
'
issuable:change
'
,
this
.
refresh
);
this
.
$wrapperEl
.
on
(
'
issuable:change
'
,
this
.
refresh
);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
addNote
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
addNote
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
,
this
.
addDiscussionNote
);
this
.
$wrapperEl
.
on
(
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
resetMainTargetForm
);
'
ajax:success
'
,
this
.
$wrapperEl
.
on
(
'
ajax:complete
'
,
'
.js-main-target-form
'
,
this
.
reenableTargetFormSubmitButton
);
'
.js-discussion-note-form
'
,
this
.
addDiscussionNote
,
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
resetMainTargetForm
,
);
this
.
$wrapperEl
.
on
(
'
ajax:complete
'
,
'
.js-main-target-form
'
,
this
.
reenableTargetFormSubmitButton
,
);
// when a key is clicked on the notes
// when a key is clicked on the notes
this
.
$wrapperEl
.
on
(
'
keydown
'
,
'
.js-note-text
'
,
this
.
keydownNoteText
);
this
.
$wrapperEl
.
on
(
'
keydown
'
,
'
.js-note-text
'
,
this
.
keydownNoteText
);
// When the URL fragment/hash has changed, `#note_xxx`
// When the URL fragment/hash has changed, `#note_xxx`
...
@@ -195,10 +258,16 @@ export default class Notes {
...
@@ -195,10 +258,16 @@ export default class Notes {
}
}
static
initCommentTypeToggle
(
form
)
{
static
initCommentTypeToggle
(
form
)
{
const
dropdownTrigger
=
form
.
querySelector
(
'
.js-comment-type-dropdown .dropdown-toggle
'
);
const
dropdownTrigger
=
form
.
querySelector
(
const
dropdownList
=
form
.
querySelector
(
'
.js-comment-type-dropdown .dropdown-menu
'
);
'
.js-comment-type-dropdown .dropdown-toggle
'
,
);
const
dropdownList
=
form
.
querySelector
(
'
.js-comment-type-dropdown .dropdown-menu
'
,
);
const
noteTypeInput
=
form
.
querySelector
(
'
#note_type
'
);
const
noteTypeInput
=
form
.
querySelector
(
'
#note_type
'
);
const
submitButton
=
form
.
querySelector
(
'
.js-comment-type-dropdown .js-comment-submit-button
'
);
const
submitButton
=
form
.
querySelector
(
'
.js-comment-type-dropdown .js-comment-submit-button
'
,
);
const
closeButton
=
form
.
querySelector
(
'
.js-note-target-close
'
);
const
closeButton
=
form
.
querySelector
(
'
.js-note-target-close
'
);
const
reopenButton
=
form
.
querySelector
(
'
.js-note-target-reopen
'
);
const
reopenButton
=
form
.
querySelector
(
'
.js-note-target-reopen
'
);
...
@@ -215,7 +284,13 @@ export default class Notes {
...
@@ -215,7 +284,13 @@ export default class Notes {
}
}
keydownNoteText
(
e
)
{
keydownNoteText
(
e
)
{
var
$textarea
,
discussionNoteForm
,
editNote
,
myLastNote
,
myLastNoteEditBtn
,
newText
,
originalText
;
var
$textarea
,
discussionNoteForm
,
editNote
,
myLastNote
,
myLastNoteEditBtn
,
newText
,
originalText
;
if
(
isMetaKey
(
e
))
{
if
(
isMetaKey
(
e
))
{
return
;
return
;
}
}
...
@@ -227,7 +302,12 @@ export default class Notes {
...
@@ -227,7 +302,12 @@ export default class Notes {
if
(
$textarea
.
val
()
!==
''
)
{
if
(
$textarea
.
val
()
!==
''
)
{
return
;
return
;
}
}
myLastNote
=
$
(
`li.note[data-author-id='
${
gon
.
current_user_id
}
'][data-editable]:last`
,
$textarea
.
closest
(
'
.note, .notes_holder, #notes
'
));
myLastNote
=
$
(
`li.note[data-author-id='
${
gon
.
current_user_id
}
'][data-editable]:last`
,
$textarea
.
closest
(
'
.note, .notes_holder, #notes
'
),
);
if
(
myLastNote
.
length
)
{
if
(
myLastNote
.
length
)
{
myLastNoteEditBtn
=
myLastNote
.
find
(
'
.js-note-edit
'
);
myLastNoteEditBtn
=
myLastNote
.
find
(
'
.js-note-edit
'
);
return
myLastNoteEditBtn
.
trigger
(
'
click
'
,
[
true
,
myLastNote
]);
return
myLastNoteEditBtn
.
trigger
(
'
click
'
,
[
true
,
myLastNote
]);
...
@@ -238,7 +318,9 @@ export default class Notes {
...
@@ -238,7 +318,9 @@ export default class Notes {
discussionNoteForm
=
$textarea
.
closest
(
'
.js-discussion-note-form
'
);
discussionNoteForm
=
$textarea
.
closest
(
'
.js-discussion-note-form
'
);
if
(
discussionNoteForm
.
length
)
{
if
(
discussionNoteForm
.
length
)
{
if
(
$textarea
.
val
()
!==
''
)
{
if
(
$textarea
.
val
()
!==
''
)
{
if
(
!
confirm
(
'
Are you sure you want to cancel creating this comment?
'
))
{
if
(
!
confirm
(
'
Are you sure you want to cancel creating this comment?
'
)
)
{
return
;
return
;
}
}
}
}
...
@@ -250,7 +332,9 @@ export default class Notes {
...
@@ -250,7 +332,9 @@ export default class Notes {
originalText
=
$textarea
.
closest
(
'
form
'
).
data
(
'
originalNote
'
);
originalText
=
$textarea
.
closest
(
'
form
'
).
data
(
'
originalNote
'
);
newText
=
$textarea
.
val
();
newText
=
$textarea
.
val
();
if
(
originalText
!==
newText
)
{
if
(
originalText
!==
newText
)
{
if
(
!
confirm
(
'
Are you sure you want to cancel editing this comment?
'
))
{
if
(
!
confirm
(
'
Are you sure you want to cancel editing this comment?
'
)
)
{
return
;
return
;
}
}
}
}
...
@@ -263,11 +347,14 @@ export default class Notes {
...
@@ -263,11 +347,14 @@ export default class Notes {
if
(
Notes
.
interval
)
{
if
(
Notes
.
interval
)
{
clearInterval
(
Notes
.
interval
);
clearInterval
(
Notes
.
interval
);
}
}
return
Notes
.
interval
=
setInterval
((
function
(
_this
)
{
return
(
Notes
.
interval
=
setInterval
(
return
function
()
{
(
function
(
_this
)
{
return
_this
.
refresh
();
return
function
()
{
};
return
_this
.
refresh
();
})(
this
),
this
.
pollingInterval
);
};
})(
this
),
this
.
pollingInterval
,
));
}
}
refresh
()
{
refresh
()
{
...
@@ -283,20 +370,23 @@ export default class Notes {
...
@@ -283,20 +370,23 @@ export default class Notes {
this
.
refreshing
=
true
;
this
.
refreshing
=
true
;
axios
.
get
(
`
${
this
.
notes_url
}
?html=true`
,
{
axios
headers
:
{
.
get
(
`
${
this
.
notes_url
}
?html=true`
,
{
'
X-Last-Fetched-At
'
:
this
.
last_fetched_at
,
headers
:
{
},
'
X-Last-Fetched-At
'
:
this
.
last_fetched_at
,
}).
then
(({
data
})
=>
{
},
const
notes
=
data
.
notes
;
})
this
.
last_fetched_at
=
data
.
last_fetched_at
;
.
then
(({
data
})
=>
{
this
.
setPollingInterval
(
data
.
notes
.
length
);
const
notes
=
data
.
notes
;
$
.
each
(
notes
,
(
i
,
note
)
=>
this
.
renderNote
(
note
));
this
.
last_fetched_at
=
data
.
last_fetched_at
;
this
.
setPollingInterval
(
data
.
notes
.
length
);
this
.
refreshing
=
false
;
$
.
each
(
notes
,
(
i
,
note
)
=>
this
.
renderNote
(
note
));
}).
catch
(()
=>
{
this
.
refreshing
=
false
;
this
.
refreshing
=
false
;
});
})
.
catch
(()
=>
{
this
.
refreshing
=
false
;
});
}
}
/**
/**
...
@@ -312,7 +402,8 @@ export default class Notes {
...
@@ -312,7 +402,8 @@ export default class Notes {
if
(
shouldReset
==
null
)
{
if
(
shouldReset
==
null
)
{
shouldReset
=
true
;
shouldReset
=
true
;
}
}
nthInterval
=
this
.
basePollingInterval
*
Math
.
pow
(
2
,
this
.
maxPollingSteps
-
1
);
nthInterval
=
this
.
basePollingInterval
*
Math
.
pow
(
2
,
this
.
maxPollingSteps
-
1
);
if
(
shouldReset
)
{
if
(
shouldReset
)
{
this
.
pollingInterval
=
this
.
basePollingInterval
;
this
.
pollingInterval
=
this
.
basePollingInterval
;
}
else
if
(
this
.
pollingInterval
<
nthInterval
)
{
}
else
if
(
this
.
pollingInterval
<
nthInterval
)
{
...
@@ -331,12 +422,17 @@ export default class Notes {
...
@@ -331,12 +422,17 @@ export default class Notes {
if
(
'
emoji_award
'
in
noteEntity
.
commands_changes
)
{
if
(
'
emoji_award
'
in
noteEntity
.
commands_changes
)
{
votesBlock
=
$
(
'
.js-awards-block
'
).
eq
(
0
);
votesBlock
=
$
(
'
.js-awards-block
'
).
eq
(
0
);
loadAwardsHandler
().
then
((
awardsHandler
)
=>
{
loadAwardsHandler
()
awardsHandler
.
addAwardToEmojiBar
(
votesBlock
,
noteEntity
.
commands_changes
.
emoji_award
);
.
then
(
awardsHandler
=>
{
awardsHandler
.
scrollToAwards
();
awardsHandler
.
addAwardToEmojiBar
(
}).
catch
(()
=>
{
votesBlock
,
// ignore
noteEntity
.
commands_changes
.
emoji_award
,
});
);
awardsHandler
.
scrollToAwards
();
})
.
catch
(()
=>
{
// ignore
});
}
}
}
}
}
}
...
@@ -381,11 +477,17 @@ export default class Notes {
...
@@ -381,11 +477,17 @@ export default class Notes {
if
(
!
noteEntity
.
valid
)
{
if
(
!
noteEntity
.
valid
)
{
if
(
noteEntity
.
errors
&&
noteEntity
.
errors
.
commands_only
)
{
if
(
noteEntity
.
errors
&&
noteEntity
.
errors
.
commands_only
)
{
if
(
noteEntity
.
commands_changes
&&
if
(
Object
.
keys
(
noteEntity
.
commands_changes
).
length
>
0
)
{
noteEntity
.
commands_changes
&&
Object
.
keys
(
noteEntity
.
commands_changes
).
length
>
0
)
{
$notesList
.
find
(
'
.system-note.being-posted
'
).
remove
();
$notesList
.
find
(
'
.system-note.being-posted
'
).
remove
();
}
}
this
.
addFlash
(
noteEntity
.
errors
.
commands_only
,
'
notice
'
,
this
.
parentTimeline
.
get
(
0
));
this
.
addFlash
(
noteEntity
.
errors
.
commands_only
,
'
notice
'
,
this
.
parentTimeline
.
get
(
0
),
);
this
.
refresh
();
this
.
refresh
();
}
}
return
;
return
;
...
@@ -407,28 +509,30 @@ export default class Notes {
...
@@ -407,28 +509,30 @@ export default class Notes {
this
.
setupNewNote
(
$newNote
);
this
.
setupNewNote
(
$newNote
);
this
.
refresh
();
this
.
refresh
();
return
this
.
updateNotesCount
(
1
);
return
this
.
updateNotesCount
(
1
);
}
}
else
if
(
Notes
.
isUpdatedNote
(
noteEntity
,
$note
))
{
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
else
if
(
Notes
.
isUpdatedNote
(
noteEntity
,
$note
))
{
const
isEditing
=
$note
.
hasClass
(
'
is-editing
'
);
const
isEditing
=
$note
.
hasClass
(
'
is-editing
'
);
const
initialContent
=
normalizeNewlines
(
const
initialContent
=
normalizeNewlines
(
$note
.
find
(
'
.original-note-content
'
).
text
().
trim
()
$note
.
find
(
'
.original-note-content
'
)
.
text
()
.
trim
(),
);
);
const
$textarea
=
$note
.
find
(
'
.js-note-text
'
);
const
$textarea
=
$note
.
find
(
'
.js-note-text
'
);
const
currentContent
=
$textarea
.
val
();
const
currentContent
=
$textarea
.
val
();
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const
sanitizedNoteNote
=
normalizeNewlines
(
noteEntity
.
note
);
const
sanitizedNoteNote
=
normalizeNewlines
(
noteEntity
.
note
);
const
isTextareaUntouched
=
currentContent
===
initialContent
||
currentContent
===
sanitizedNoteNote
;
const
isTextareaUntouched
=
currentContent
===
initialContent
||
currentContent
===
sanitizedNoteNote
;
if
(
isEditing
&&
isTextareaUntouched
)
{
if
(
isEditing
&&
isTextareaUntouched
)
{
$textarea
.
val
(
noteEntity
.
note
);
$textarea
.
val
(
noteEntity
.
note
);
this
.
updatedNotesTrackingMap
[
noteEntity
.
id
]
=
noteEntity
;
this
.
updatedNotesTrackingMap
[
noteEntity
.
id
]
=
noteEntity
;
}
}
else
if
(
isEditing
&&
!
isTextareaUntouched
)
{
else
if
(
isEditing
&&
!
isTextareaUntouched
)
{
this
.
putConflictEditWarningInPlace
(
noteEntity
,
$note
);
this
.
putConflictEditWarningInPlace
(
noteEntity
,
$note
);
this
.
updatedNotesTrackingMap
[
noteEntity
.
id
]
=
noteEntity
;
this
.
updatedNotesTrackingMap
[
noteEntity
.
id
]
=
noteEntity
;
}
}
else
{
else
{
const
$updatedNote
=
Notes
.
animateUpdateNote
(
noteEntity
.
html
,
$note
);
const
$updatedNote
=
Notes
.
animateUpdateNote
(
noteEntity
.
html
,
$note
);
this
.
setupNewNote
(
$updatedNote
);
this
.
setupNewNote
(
$updatedNote
);
}
}
...
@@ -452,17 +556,31 @@ export default class Notes {
...
@@ -452,17 +556,31 @@ export default class Notes {
}
}
this
.
note_ids
.
push
(
noteEntity
.
id
);
this
.
note_ids
.
push
(
noteEntity
.
id
);
form
=
$form
||
$
(
`.js-discussion-note-form[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
);
form
=
row
=
(
form
.
length
||
!
noteEntity
.
discussion_line_code
)
?
form
.
closest
(
'
tr
'
)
:
$
(
`#
${
noteEntity
.
discussion_line_code
}
`
);
$form
||
$
(
`.js-discussion-note-form[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
,
);
row
=
form
.
length
||
!
noteEntity
.
discussion_line_code
?
form
.
closest
(
'
tr
'
)
:
$
(
`#
${
noteEntity
.
discussion_line_code
}
`
);
if
(
noteEntity
.
on_image
)
{
if
(
noteEntity
.
on_image
)
{
row
=
form
;
row
=
form
;
}
}
lineType
=
this
.
isParallelView
()
?
form
.
find
(
'
#line_type
'
).
val
()
:
'
old
'
;
lineType
=
this
.
isParallelView
()
?
form
.
find
(
'
#line_type
'
).
val
()
:
'
old
'
;
diffAvatarContainer
=
row
.
prevAll
(
'
.line_holder
'
).
first
().
find
(
'
.js-avatar-container.
'
+
lineType
+
'
_line
'
);
diffAvatarContainer
=
row
.
prevAll
(
'
.line_holder
'
)
.
first
()
.
find
(
'
.js-avatar-container.
'
+
lineType
+
'
_line
'
);
// is this the first note of discussion?
// is this the first note of discussion?
discussionContainer
=
$
(
`.notes[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
);
discussionContainer
=
$
(
`.notes[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
,
);
if
(
!
discussionContainer
.
length
)
{
if
(
!
discussionContainer
.
length
)
{
discussionContainer
=
form
.
closest
(
'
.discussion
'
).
find
(
'
.notes
'
);
discussionContainer
=
form
.
closest
(
'
.discussion
'
).
find
(
'
.notes
'
);
}
}
...
@@ -470,25 +588,42 @@ export default class Notes {
...
@@ -470,25 +588,42 @@ export default class Notes {
if
(
noteEntity
.
diff_discussion_html
)
{
if
(
noteEntity
.
diff_discussion_html
)
{
var
$discussion
=
$
(
noteEntity
.
diff_discussion_html
).
renderGFM
();
var
$discussion
=
$
(
noteEntity
.
diff_discussion_html
).
renderGFM
();
if
(
!
this
.
isParallelView
()
||
row
.
hasClass
(
'
js-temp-notes-holder
'
)
||
noteEntity
.
on_image
)
{
if
(
!
this
.
isParallelView
()
||
row
.
hasClass
(
'
js-temp-notes-holder
'
)
||
noteEntity
.
on_image
)
{
// insert the note and the reply button after the temp row
// insert the note and the reply button after the temp row
row
.
after
(
$discussion
);
row
.
after
(
$discussion
);
}
else
{
}
else
{
// Merge new discussion HTML in
// Merge new discussion HTML in
var
$notes
=
$discussion
.
find
(
`.notes[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
);
var
$notes
=
$discussion
.
find
(
var
contentContainerClass
=
'
.
'
+
$notes
.
closest
(
'
.notes_content
'
)
`.notes[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
,
.
attr
(
'
class
'
)
);
.
split
(
'
'
)
var
contentContainerClass
=
.
join
(
'
.
'
);
'
.
'
+
$notes
row
.
find
(
contentContainerClass
+
'
.content
'
).
append
(
$notes
.
closest
(
'
.content
'
).
children
());
.
closest
(
'
.notes_content
'
)
.
attr
(
'
class
'
)
.
split
(
'
'
)
.
join
(
'
.
'
);
row
.
find
(
contentContainerClass
+
'
.content
'
)
.
append
(
$notes
.
closest
(
'
.content
'
).
children
());
}
}
}
}
// Init discussion on 'Discussion' page if it is merge request page
// Init discussion on 'Discussion' page if it is merge request page
const
page
=
$
(
'
body
'
).
attr
(
'
data-page
'
);
const
page
=
$
(
'
body
'
).
attr
(
'
data-page
'
);
if
((
page
&&
page
.
indexOf
(
'
projects:merge_request
'
)
!==
-
1
)
||
!
noteEntity
.
diff_discussion_html
)
{
if
(
(
page
&&
page
.
indexOf
(
'
projects:merge_request
'
)
!==
-
1
)
||
!
noteEntity
.
diff_discussion_html
)
{
if
(
!
hasVueMRDiscussionsCookie
())
{
if
(
!
hasVueMRDiscussionsCookie
())
{
Notes
.
animateAppendNote
(
noteEntity
.
discussion_html
,
$
(
'
.main-notes-list
'
));
Notes
.
animateAppendNote
(
noteEntity
.
discussion_html
,
$
(
'
.main-notes-list
'
),
);
}
}
}
}
}
else
{
}
else
{
...
@@ -496,7 +631,10 @@ export default class Notes {
...
@@ -496,7 +631,10 @@ export default class Notes {
Notes
.
animateAppendNote
(
noteEntity
.
html
,
discussionContainer
);
Notes
.
animateAppendNote
(
noteEntity
.
html
,
discussionContainer
);
}
}
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
&&
noteEntity
.
discussion_resolvable
)
{
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
&&
noteEntity
.
discussion_resolvable
)
{
gl
.
diffNotesCompileComponents
();
gl
.
diffNotesCompileComponents
();
this
.
renderDiscussionAvatar
(
diffAvatarContainer
,
noteEntity
);
this
.
renderDiscussionAvatar
(
diffAvatarContainer
,
noteEntity
);
...
@@ -508,7 +646,8 @@ export default class Notes {
...
@@ -508,7 +646,8 @@ export default class Notes {
}
}
getLineHolder
(
changesDiscussionContainer
)
{
getLineHolder
(
changesDiscussionContainer
)
{
return
$
(
changesDiscussionContainer
).
closest
(
'
.notes_holder
'
)
return
$
(
changesDiscussionContainer
)
.
closest
(
'
.notes_holder
'
)
.
prevAll
(
'
.line_holder
'
)
.
prevAll
(
'
.line_holder
'
)
.
first
()
.
first
()
.
get
(
0
);
.
get
(
0
);
...
@@ -541,8 +680,14 @@ export default class Notes {
...
@@ -541,8 +680,14 @@ export default class Notes {
form
.
find
(
'
.js-errors
'
).
remove
();
form
.
find
(
'
.js-errors
'
).
remove
();
// reset text and preview
// reset text and preview
form
.
find
(
'
.js-md-write-button
'
).
click
();
form
.
find
(
'
.js-md-write-button
'
).
click
();
form
.
find
(
'
.js-note-text
'
).
val
(
''
).
trigger
(
'
input
'
);
form
form
.
find
(
'
.js-note-text
'
).
data
(
'
autosave
'
).
reset
();
.
find
(
'
.js-note-text
'
)
.
val
(
''
)
.
trigger
(
'
input
'
);
form
.
find
(
'
.js-note-text
'
)
.
data
(
'
autosave
'
)
.
reset
();
var
event
=
document
.
createEvent
(
'
Event
'
);
var
event
=
document
.
createEvent
(
'
Event
'
);
event
.
initEvent
(
'
autosize:update
'
,
true
,
false
);
event
.
initEvent
(
'
autosize:update
'
,
true
,
false
);
...
@@ -578,7 +723,10 @@ export default class Notes {
...
@@ -578,7 +723,10 @@ export default class Notes {
form
.
find
(
'
#note_type
'
).
val
(
''
);
form
.
find
(
'
#note_type
'
).
val
(
''
);
form
.
find
(
'
#note_project_id
'
).
remove
();
form
.
find
(
'
#note_project_id
'
).
remove
();
form
.
find
(
'
#in_reply_to_discussion_id
'
).
remove
();
form
.
find
(
'
#in_reply_to_discussion_id
'
).
remove
();
form
.
find
(
'
.js-comment-resolve-button
'
).
closest
(
'
comment-and-resolve-btn
'
).
remove
();
form
.
find
(
'
.js-comment-resolve-button
'
)
.
closest
(
'
comment-and-resolve-btn
'
)
.
remove
();
this
.
parentTimeline
=
form
.
parents
(
'
.timeline
'
);
this
.
parentTimeline
=
form
.
parents
(
'
.timeline
'
);
if
(
form
.
length
)
{
if
(
form
.
length
)
{
...
@@ -632,11 +780,17 @@ export default class Notes {
...
@@ -632,11 +780,17 @@ export default class Notes {
}
else
if
(
$form
.
hasClass
(
'
js-discussion-note-form
'
))
{
}
else
if
(
$form
.
hasClass
(
'
js-discussion-note-form
'
))
{
formParentTimeline
=
$form
.
closest
(
'
.discussion-notes
'
).
find
(
'
.notes
'
);
formParentTimeline
=
$form
.
closest
(
'
.discussion-notes
'
).
find
(
'
.notes
'
);
}
}
return
this
.
addFlash
(
'
Your comment could not be submitted! Please check your network connection and try again.
'
,
'
alert
'
,
formParentTimeline
.
get
(
0
));
return
this
.
addFlash
(
'
Your comment could not be submitted! Please check your network connection and try again.
'
,
'
alert
'
,
formParentTimeline
.
get
(
0
),
);
}
}
updateNoteError
(
$parentTimeline
)
{
updateNoteError
(
$parentTimeline
)
{
new
Flash
(
'
Your comment could not be updated! Please check your network connection and try again.
'
);
new
Flash
(
'
Your comment could not be updated! Please check your network connection and try again.
'
,
);
}
}
/**
/**
...
@@ -685,14 +839,16 @@ export default class Notes {
...
@@ -685,14 +839,16 @@ export default class Notes {
}
}
checkContentToAllowEditing
(
$el
)
{
checkContentToAllowEditing
(
$el
)
{
var
initialContent
=
$el
.
find
(
'
.original-note-content
'
).
text
().
trim
();
var
initialContent
=
$el
.
find
(
'
.original-note-content
'
)
.
text
()
.
trim
();
var
currentContent
=
$el
.
find
(
'
.js-note-text
'
).
val
();
var
currentContent
=
$el
.
find
(
'
.js-note-text
'
).
val
();
var
isAllowed
=
true
;
var
isAllowed
=
true
;
if
(
currentContent
===
initialContent
)
{
if
(
currentContent
===
initialContent
)
{
this
.
removeNoteEditForm
(
$el
);
this
.
removeNoteEditForm
(
$el
);
}
}
else
{
else
{
var
$buttons
=
$el
.
find
(
'
.note-form-actions
'
);
var
$buttons
=
$el
.
find
(
'
.note-form-actions
'
);
var
isWidgetVisible
=
isInViewport
(
$el
.
get
(
0
));
var
isWidgetVisible
=
isInViewport
(
$el
.
get
(
0
));
...
@@ -754,8 +910,7 @@ export default class Notes {
...
@@ -754,8 +910,7 @@ export default class Notes {
this
.
setupNewNote
(
$newNote
);
this
.
setupNewNote
(
$newNote
);
// Now that we have taken care of the update, clear it out
// Now that we have taken care of the update, clear it out
delete
this
.
updatedNotesTrackingMap
[
noteId
];
delete
this
.
updatedNotesTrackingMap
[
noteId
];
}
}
else
{
else
{
$note
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
$note
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
this
.
removeNoteEditForm
(
$note
);
this
.
removeNoteEditForm
(
$note
);
}
}
...
@@ -788,7 +943,9 @@ export default class Notes {
...
@@ -788,7 +943,9 @@ export default class Notes {
form
.
removeClass
(
'
current-note-edit-form
'
);
form
.
removeClass
(
'
current-note-edit-form
'
);
form
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
form
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
// Replace markdown textarea text with original note text.
// Replace markdown textarea text with original note text.
return
form
.
find
(
'
.js-note-text
'
).
val
(
form
.
find
(
'
form.edit-note
'
).
data
(
'
originalNote
'
));
return
form
.
find
(
'
.js-note-text
'
)
.
val
(
form
.
find
(
'
form.edit-note
'
).
data
(
'
originalNote
'
));
}
}
/**
/**
...
@@ -802,58 +959,67 @@ export default class Notes {
...
@@ -802,58 +959,67 @@ export default class Notes {
$note
=
$
(
e
.
currentTarget
).
closest
(
'
.note
'
);
$note
=
$
(
e
.
currentTarget
).
closest
(
'
.note
'
);
noteElId
=
$note
.
attr
(
'
id
'
);
noteElId
=
$note
.
attr
(
'
id
'
);
noteId
=
$note
.
attr
(
'
data-note-id
'
);
noteId
=
$note
.
attr
(
'
data-note-id
'
);
lineHolder
=
$
(
e
.
currentTarget
).
closest
(
'
.notes[data-discussion-id]
'
)
lineHolder
=
$
(
e
.
currentTarget
)
.
closest
(
'
.notes[data-discussion-id]
'
)
.
closest
(
'
.notes_holder
'
)
.
closest
(
'
.notes_holder
'
)
.
prev
(
'
.line_holder
'
);
.
prev
(
'
.line_holder
'
);
$
(
`.note[id="
${
noteElId
}
"]`
).
each
((
function
(
_this
)
{
$
(
`.note[id="
${
noteElId
}
"]`
).
each
(
// A same note appears in the "Discussion" and in the "Changes" tab, we have
(
function
(
_this
)
{
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// where $('#noteId') would return only one.
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
return
function
(
i
,
el
)
{
// where $('#noteId') would return only one.
var
$note
,
$notes
;
return
function
(
i
,
el
)
{
$note
=
$
(
el
);
var
$note
,
$notes
;
$notes
=
$note
.
closest
(
'
.discussion-notes
'
);
$note
=
$
(
el
);
const
discussionId
=
$
(
'
.notes
'
,
$notes
).
data
(
'
discussionId
'
);
$notes
=
$note
.
closest
(
'
.discussion-notes
'
);
const
discussionId
=
$
(
'
.notes
'
,
$notes
).
data
(
'
discussionId
'
);
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
)
{
if
(
gl
.
diffNoteApps
[
noteElId
])
{
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
)
{
gl
.
diffNoteApps
[
noteElId
].
$destroy
();
if
(
gl
.
diffNoteApps
[
noteElId
])
{
gl
.
diffNoteApps
[
noteElId
].
$destroy
();
}
}
}
}
$note
.
remove
();
// check if this is the last note for this line
if
(
$notes
.
find
(
'
.note
'
).
length
===
0
)
{
var
notesTr
=
$notes
.
closest
(
'
tr
'
);
// "Discussions" tab
$notes
.
closest
(
'
.timeline-entry
'
).
remove
();
$
(
`.js-diff-avatars-
${
discussionId
}
`
).
trigger
(
'
remove.vue
'
);
// The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs
if
(
notesTr
.
find
(
'
.discussion-notes
'
).
length
>
1
||
notesTr
.
length
===
0
)
{
const
$diffFile
=
$notes
.
closest
(
'
.diff-file
'
);
if
(
$diffFile
.
length
>
0
)
{
const
removeBadgeEvent
=
new
CustomEvent
(
'
removeBadge.imageDiff
'
,
{
detail
:
{
// badgeNumber's start with 1 and index starts with 0
badgeNumber
:
$notes
.
index
()
+
1
,
},
});
$diffFile
[
0
].
dispatchEvent
(
removeBadgeEvent
);
$note
.
remove
();
// check if this is the last note for this line
if
(
$notes
.
find
(
'
.note
'
).
length
===
0
)
{
var
notesTr
=
$notes
.
closest
(
'
tr
'
);
// "Discussions" tab
$notes
.
closest
(
'
.timeline-entry
'
).
remove
();
$
(
`.js-diff-avatars-
${
discussionId
}
`
).
trigger
(
'
remove.vue
'
);
// The notes tr can contain multiple lists of notes, like on the parallel diff
// notesTr does not exist for image diffs
if
(
notesTr
.
find
(
'
.discussion-notes
'
).
length
>
1
||
notesTr
.
length
===
0
)
{
const
$diffFile
=
$notes
.
closest
(
'
.diff-file
'
);
if
(
$diffFile
.
length
>
0
)
{
const
removeBadgeEvent
=
new
CustomEvent
(
'
removeBadge.imageDiff
'
,
{
detail
:
{
// badgeNumber's start with 1 and index starts with 0
badgeNumber
:
$notes
.
index
()
+
1
,
},
},
);
$diffFile
[
0
].
dispatchEvent
(
removeBadgeEvent
);
}
$notes
.
remove
();
}
else
if
(
notesTr
.
length
>
0
)
{
notesTr
.
remove
();
}
}
$notes
.
remove
();
}
else
if
(
notesTr
.
length
>
0
)
{
notesTr
.
remove
();
}
}
}
}
;
}
;
}
)(
this
),
})(
this
)
);
);
Notes
.
refreshVueNotes
();
Notes
.
refreshVueNotes
();
Notes
.
checkMergeRequestStatus
();
Notes
.
checkMergeRequestStatus
();
...
@@ -935,7 +1101,12 @@ export default class Notes {
...
@@ -935,7 +1101,12 @@ export default class Notes {
// DiffNote
// DiffNote
form
.
find
(
'
#note_position
'
).
val
(
dataHolder
.
attr
(
'
data-position
'
));
form
.
find
(
'
#note_position
'
).
val
(
dataHolder
.
attr
(
'
data-position
'
));
form
.
find
(
'
.js-note-discard
'
).
show
().
removeClass
(
'
js-note-discard
'
).
addClass
(
'
js-close-discussion-note-form
'
).
text
(
form
.
find
(
'
.js-close-discussion-note-form
'
).
data
(
'
cancelText
'
));
form
.
find
(
'
.js-note-discard
'
)
.
show
()
.
removeClass
(
'
js-note-discard
'
)
.
addClass
(
'
js-close-discussion-note-form
'
)
.
text
(
form
.
find
(
'
.js-close-discussion-note-form
'
).
data
(
'
cancelText
'
));
form
.
find
(
'
.js-note-target-close
'
).
remove
();
form
.
find
(
'
.js-note-target-close
'
).
remove
();
form
.
find
(
'
.js-note-new-discussion
'
).
remove
();
form
.
find
(
'
.js-note-new-discussion
'
).
remove
();
this
.
setupNoteForm
(
form
);
this
.
setupNoteForm
(
form
);
...
@@ -971,7 +1142,7 @@ export default class Notes {
...
@@ -971,7 +1142,7 @@ export default class Notes {
this
.
toggleDiffNote
({
this
.
toggleDiffNote
({
target
:
$link
,
target
:
$link
,
lineType
:
link
.
dataset
.
lineType
,
lineType
:
link
.
dataset
.
lineType
,
showReplyInput
showReplyInput
,
});
});
}
}
...
@@ -987,7 +1158,9 @@ export default class Notes {
...
@@ -987,7 +1158,9 @@ export default class Notes {
// Setup comment form
// Setup comment form
let
newForm
;
let
newForm
;
const
$noteContainer
=
$link
.
closest
(
'
.diff-viewer
'
).
find
(
'
.note-container
'
);
const
$noteContainer
=
$link
.
closest
(
'
.diff-viewer
'
)
.
find
(
'
.note-container
'
);
const
$form
=
$noteContainer
.
find
(
'
> .discussion-form
'
);
const
$form
=
$noteContainer
.
find
(
'
> .discussion-form
'
);
if
(
$form
.
length
===
0
)
{
if
(
$form
.
length
===
0
)
{
...
@@ -1000,13 +1173,17 @@ export default class Notes {
...
@@ -1000,13 +1173,17 @@ export default class Notes {
this
.
setupDiscussionNoteForm
(
$link
,
newForm
);
this
.
setupDiscussionNoteForm
(
$link
,
newForm
);
}
}
toggleDiffNote
({
toggleDiffNote
({
target
,
lineType
,
forceShow
,
showReplyInput
=
false
})
{
target
,
var
$link
,
lineType
,
addForm
,
forceShow
,
hasNotes
,
showReplyInput
=
false
,
newForm
,
})
{
noteForm
,
var
$link
,
addForm
,
hasNotes
,
newForm
,
noteForm
,
replyButton
,
row
,
rowCssToAdd
,
targetContent
,
isDiffCommentAvatar
;
replyButton
,
row
,
rowCssToAdd
,
targetContent
,
isDiffCommentAvatar
;
$link
=
$
(
target
);
$link
=
$
(
target
);
row
=
$link
.
closest
(
'
tr
'
);
row
=
$link
.
closest
(
'
tr
'
);
const
nextRow
=
row
.
next
();
const
nextRow
=
row
.
next
();
...
@@ -1018,11 +1195,13 @@ export default class Notes {
...
@@ -1018,11 +1195,13 @@ export default class Notes {
hasNotes
=
nextRow
.
is
(
'
.notes_holder
'
);
hasNotes
=
nextRow
.
is
(
'
.notes_holder
'
);
addForm
=
false
;
addForm
=
false
;
let
lineTypeSelector
=
''
;
let
lineTypeSelector
=
''
;
rowCssToAdd
=
'
<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>
'
;
rowCssToAdd
=
'
<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>
'
;
// In parallel view, look inside the correct left/right pane
// In parallel view, look inside the correct left/right pane
if
(
this
.
isParallelView
())
{
if
(
this
.
isParallelView
())
{
lineTypeSelector
=
`.
${
lineType
}
`
;
lineTypeSelector
=
`.
${
lineType
}
`
;
rowCssToAdd
=
'
<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>
'
;
rowCssToAdd
=
'
<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>
'
;
}
}
const
notesContentSelector
=
`.notes_content
${
lineTypeSelector
}
.content`
;
const
notesContentSelector
=
`.notes_content
${
lineTypeSelector
}
.content`
;
let
notesContent
=
targetRow
.
find
(
notesContentSelector
);
let
notesContent
=
targetRow
.
find
(
notesContentSelector
);
...
@@ -1050,7 +1229,9 @@ export default class Notes {
...
@@ -1050,7 +1229,9 @@ export default class Notes {
notesContent
=
targetRow
.
find
(
notesContentSelector
);
notesContent
=
targetRow
.
find
(
notesContentSelector
);
addForm
=
true
;
addForm
=
true
;
}
else
{
}
else
{
const
isCurrentlyShown
=
targetRow
.
find
(
'
.content:not(:empty)
'
).
is
(
'
:visible
'
);
const
isCurrentlyShown
=
targetRow
.
find
(
'
.content:not(:empty)
'
)
.
is
(
'
:visible
'
);
const
isForced
=
forceShow
===
true
||
forceShow
===
false
;
const
isForced
=
forceShow
===
true
||
forceShow
===
false
;
const
showNow
=
forceShow
===
true
||
(
!
isCurrentlyShown
&&
!
isForced
);
const
showNow
=
forceShow
===
true
||
(
!
isCurrentlyShown
&&
!
isForced
);
...
@@ -1077,11 +1258,12 @@ export default class Notes {
...
@@ -1077,11 +1258,12 @@ export default class Notes {
row
=
form
.
closest
(
'
tr
'
);
row
=
form
.
closest
(
'
tr
'
);
glForm
=
form
.
data
(
'
glForm
'
);
glForm
=
form
.
data
(
'
glForm
'
);
glForm
.
destroy
();
glForm
.
destroy
();
form
.
find
(
'
.js-note-text
'
).
data
(
'
autosave
'
).
reset
();
// show the reply button (will only work for replies)
form
form
.
prev
(
'
.discussion-reply-holder
'
)
.
find
(
'
.js-note-text
'
)
.
show
();
.
data
(
'
autosave
'
)
.
reset
();
// show the reply button (will only work for replies)
form
.
prev
(
'
.discussion-reply-holder
'
).
show
();
if
(
row
.
is
(
'
.js-temp-notes-holder
'
))
{
if
(
row
.
is
(
'
.js-temp-notes-holder
'
))
{
// remove temporary row for diff lines
// remove temporary row for diff lines
return
row
.
remove
();
return
row
.
remove
();
...
@@ -1122,7 +1304,9 @@ export default class Notes {
...
@@ -1122,7 +1304,9 @@ export default class Notes {
var
filename
,
form
;
var
filename
,
form
;
form
=
$
(
this
).
closest
(
'
form
'
);
form
=
$
(
this
).
closest
(
'
form
'
);
// get only the basename
// get only the basename
filename
=
$
(
this
).
val
().
replace
(
/^.*
[\\\/]
/
,
''
);
filename
=
$
(
this
)
.
val
()
.
replace
(
/^.*
[\\\/]
/
,
''
);
return
form
.
find
(
'
.js-attachment-filename
'
).
text
(
filename
);
return
form
.
find
(
'
.js-attachment-filename
'
).
text
(
filename
);
}
}
...
@@ -1194,12 +1378,16 @@ export default class Notes {
...
@@ -1194,12 +1378,16 @@ export default class Notes {
this
.
glForm
=
new
GLForm
(
$editForm
.
find
(
'
form
'
),
this
.
enableGFM
);
this
.
glForm
=
new
GLForm
(
$editForm
.
find
(
'
form
'
),
this
.
enableGFM
);
$editForm
.
find
(
'
form
'
)
$editForm
.
find
(
'
form
'
)
.
attr
(
'
action
'
,
`
${
postUrl
}
?html=true`
)
.
attr
(
'
action
'
,
`
${
postUrl
}
?html=true`
)
.
attr
(
'
data-remote
'
,
'
true
'
);
.
attr
(
'
data-remote
'
,
'
true
'
);
$editForm
.
find
(
'
.js-form-target-id
'
).
val
(
targetId
);
$editForm
.
find
(
'
.js-form-target-id
'
).
val
(
targetId
);
$editForm
.
find
(
'
.js-form-target-type
'
).
val
(
targetType
);
$editForm
.
find
(
'
.js-form-target-type
'
).
val
(
targetType
);
$editForm
.
find
(
'
.js-note-text
'
).
focus
().
val
(
originalContent
);
$editForm
.
find
(
'
.js-note-text
'
)
.
focus
()
.
val
(
originalContent
);
$editForm
.
find
(
'
.js-md-write-button
'
).
trigger
(
'
click
'
);
$editForm
.
find
(
'
.js-md-write-button
'
).
trigger
(
'
click
'
);
$editForm
.
find
(
'
.referenced-users
'
).
hide
();
$editForm
.
find
(
'
.referenced-users
'
).
hide
();
}
}
...
@@ -1208,7 +1396,9 @@ export default class Notes {
...
@@ -1208,7 +1396,9 @@ export default class Notes {
if
(
$note
.
find
(
'
.js-conflict-edit-warning
'
).
length
===
0
)
{
if
(
$note
.
find
(
'
.js-conflict-edit-warning
'
).
length
===
0
)
{
const
$alert
=
$
(
`<div class="js-conflict-edit-warning alert alert-danger">
const
$alert
=
$
(
`<div class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
This comment has changed since you started editing, please review the
<a href="#note_
${
noteEntity
.
id
}
" target="_blank" rel="noopener noreferrer">
<a href="#note_
${
noteEntity
.
id
}
" target="_blank" rel="noopener noreferrer">
updated comment
updated comment
</a>
</a>
to ensure information is not lost
to ensure information is not lost
...
@@ -1218,12 +1408,15 @@ export default class Notes {
...
@@ -1218,12 +1408,15 @@ export default class Notes {
}
}
updateNotesCount
(
updateCount
)
{
updateNotesCount
(
updateCount
)
{
return
this
.
notesCountBadge
.
text
(
parseInt
(
this
.
notesCountBadge
.
text
(),
10
)
+
updateCount
);
return
this
.
notesCountBadge
.
text
(
parseInt
(
this
.
notesCountBadge
.
text
(),
10
)
+
updateCount
,
);
}
}
static
renderPlaceholderComponent
(
$container
)
{
static
renderPlaceholderComponent
(
$container
)
{
const
el
=
$container
.
find
(
'
.js-code-placeholder
'
).
get
(
0
);
const
el
=
$container
.
find
(
'
.js-code-placeholder
'
).
get
(
0
);
new
Vue
({
// eslint-disable-line no-new
new
Vue
({
// eslint-disable-line no-new
el
,
el
,
components
:
{
components
:
{
SkeletonLoadingContainer
,
SkeletonLoadingContainer
,
...
@@ -1248,7 +1441,9 @@ export default class Notes {
...
@@ -1248,7 +1441,9 @@ export default class Notes {
$container
.
find
(
'
.line_content
'
).
html
(
$container
.
find
(
'
.line_content
'
).
html
(
$
(
`
$
(
`
<div class="nothing-here-block">
<div class="nothing-here-block">
${
__
(
'
Unable to load the diff.
'
)}
<a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
${
__
(
'
Unable to load the diff.
'
,
)}
<a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
</div>
</div>
`
),
`
),
);
);
...
@@ -1266,7 +1461,8 @@ export default class Notes {
...
@@ -1266,7 +1461,8 @@ export default class Notes {
const
fileHolder
=
$container
.
find
(
'
.file-holder
'
);
const
fileHolder
=
$container
.
find
(
'
.file-holder
'
);
const
url
=
fileHolder
.
data
(
'
linesPath
'
);
const
url
=
fileHolder
.
data
(
'
linesPath
'
);
axios
.
get
(
url
)
axios
.
get
(
url
)
.
then
(({
data
})
=>
{
.
then
(({
data
})
=>
{
Notes
.
renderDiffContent
(
$container
,
data
);
Notes
.
renderDiffContent
(
$container
,
data
);
})
})
...
@@ -1277,9 +1473,14 @@ export default class Notes {
...
@@ -1277,9 +1473,14 @@ export default class Notes {
toggleCommitList
(
e
)
{
toggleCommitList
(
e
)
{
const
$element
=
$
(
e
.
currentTarget
);
const
$element
=
$
(
e
.
currentTarget
);
const
$closestSystemCommitList
=
$element
.
siblings
(
'
.system-note-commit-list
'
);
const
$closestSystemCommitList
=
$element
.
siblings
(
'
.system-note-commit-list
'
,
);
$element
.
find
(
'
.fa
'
).
toggleClass
(
'
fa-angle-down
'
).
toggleClass
(
'
fa-angle-up
'
);
$element
.
find
(
'
.fa
'
)
.
toggleClass
(
'
fa-angle-down
'
)
.
toggleClass
(
'
fa-angle-up
'
);
$closestSystemCommitList
.
toggleClass
(
'
hide-shade
'
);
$closestSystemCommitList
.
toggleClass
(
'
hide-shade
'
);
}
}
...
@@ -1289,11 +1490,17 @@ export default class Notes {
...
@@ -1289,11 +1490,17 @@ export default class Notes {
* intrusive.
* intrusive.
*/
*/
collapseLongCommitList
()
{
collapseLongCommitList
()
{
const
systemNotes
=
$
(
'
#notes-list
'
).
find
(
'
li.system-note
'
).
has
(
'
ul
'
);
const
systemNotes
=
$
(
'
#notes-list
'
)
.
find
(
'
li.system-note
'
)
.
has
(
'
ul
'
);
$
.
each
(
systemNotes
,
function
(
index
,
systemNote
)
{
$
.
each
(
systemNotes
,
function
(
index
,
systemNote
)
{
const
$systemNote
=
$
(
systemNote
);
const
$systemNote
=
$
(
systemNote
);
const
headerMessage
=
$systemNote
.
find
(
'
.note-text
'
).
find
(
'
p:first
'
).
text
().
replace
(
'
:
'
,
''
);
const
headerMessage
=
$systemNote
.
find
(
'
.note-text
'
)
.
find
(
'
p:first
'
)
.
text
()
.
replace
(
'
:
'
,
''
);
$systemNote
.
find
(
'
.note-header .system-note-message
'
).
html
(
headerMessage
);
$systemNote
.
find
(
'
.note-header .system-note-message
'
).
html
(
headerMessage
);
...
@@ -1301,7 +1508,9 @@ export default class Notes {
...
@@ -1301,7 +1508,9 @@ export default class Notes {
$systemNote
.
find
(
'
.note-text
'
).
addClass
(
'
system-note-commit-list
'
);
$systemNote
.
find
(
'
.note-text
'
).
addClass
(
'
system-note-commit-list
'
);
$systemNote
.
find
(
'
.system-note-commit-list-toggler
'
).
show
();
$systemNote
.
find
(
'
.system-note-commit-list-toggler
'
).
show
();
}
else
{
}
else
{
$systemNote
.
find
(
'
.note-text
'
).
addClass
(
'
system-note-commit-list hide-shade
'
);
$systemNote
.
find
(
'
.note-text
'
)
.
addClass
(
'
system-note-commit-list hide-shade
'
);
}
}
});
});
}
}
...
@@ -1319,14 +1528,10 @@ export default class Notes {
...
@@ -1319,14 +1528,10 @@ export default class Notes {
cleanForm
(
$form
)
{
cleanForm
(
$form
)
{
// Remove JS classes that are not needed here
// Remove JS classes that are not needed here
$form
$form
.
find
(
'
.js-comment-type-dropdown
'
).
removeClass
(
'
btn-group
'
);
.
find
(
'
.js-comment-type-dropdown
'
)
.
removeClass
(
'
btn-group
'
);
// Remove dropdown
// Remove dropdown
$form
$form
.
find
(
'
.dropdown-menu
'
).
remove
();
.
find
(
'
.dropdown-menu
'
)
.
remove
();
return
$form
;
return
$form
;
}
}
...
@@ -1345,7 +1550,11 @@ export default class Notes {
...
@@ -1345,7 +1550,11 @@ export default class Notes {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const
sanitizedNoteEntityText
=
normalizeNewlines
(
noteEntity
.
note
.
trim
());
const
sanitizedNoteEntityText
=
normalizeNewlines
(
noteEntity
.
note
.
trim
());
const
currentNoteText
=
normalizeNewlines
(
const
currentNoteText
=
normalizeNewlines
(
$note
.
find
(
'
.original-note-content
'
).
first
().
text
().
trim
()
$note
.
find
(
'
.original-note-content
'
)
.
first
()
.
text
()
.
trim
(),
);
);
return
sanitizedNoteEntityText
!==
currentNoteText
;
return
sanitizedNoteEntityText
!==
currentNoteText
;
}
}
...
@@ -1435,7 +1644,14 @@ export default class Notes {
...
@@ -1435,7 +1644,14 @@ export default class Notes {
* Once comment is _actually_ posted on server, we will have final element
* Once comment is _actually_ posted on server, we will have final element
* in response that we will show in place of this temporary element.
* in response that we will show in place of this temporary element.
*/
*/
createPlaceholderNote
({
formContent
,
uniqueId
,
isDiscussionNote
,
currentUsername
,
currentUserFullname
,
currentUserAvatar
})
{
createPlaceholderNote
({
formContent
,
uniqueId
,
isDiscussionNote
,
currentUsername
,
currentUserFullname
,
currentUserAvatar
,
})
{
const
discussionClass
=
isDiscussionNote
?
'
discussion
'
:
''
;
const
discussionClass
=
isDiscussionNote
?
'
discussion
'
:
''
;
const
$tempNote
=
$
(
const
$tempNote
=
$
(
`<li id="
${
uniqueId
}
" class="note being-posted fade-in-half timeline-entry">
`<li id="
${
uniqueId
}
" class="note being-posted fade-in-half timeline-entry">
...
@@ -1449,8 +1665,12 @@ export default class Notes {
...
@@ -1449,8 +1665,12 @@ export default class Notes {
<div class="note-header">
<div class="note-header">
<div class="note-header-info">
<div class="note-header-info">
<a href="/
${
_
.
escape
(
currentUsername
)}
">
<a href="/
${
_
.
escape
(
currentUsername
)}
">
<span class="hidden-xs">
${
_
.
escape
(
currentUsername
)}
</span>
<span class="hidden-xs">
${
_
.
escape
(
<span class="note-headline-light">
${
_
.
escape
(
currentUsername
)}
</span>
currentUsername
,
)}
</span>
<span class="note-headline-light">
${
_
.
escape
(
currentUsername
,
)}
</span>
</a>
</a>
</div>
</div>
</div>
</div>
...
@@ -1461,11 +1681,13 @@ export default class Notes {
...
@@ -1461,11 +1681,13 @@ export default class Notes {
</div>
</div>
</div>
</div>
</div>
</div>
</li>`
</li>`
,
);
);
$tempNote
.
find
(
'
.hidden-xs
'
).
text
(
_
.
escape
(
currentUserFullname
));
$tempNote
.
find
(
'
.hidden-xs
'
).
text
(
_
.
escape
(
currentUserFullname
));
$tempNote
.
find
(
'
.note-headline-light
'
).
text
(
`@
${
_
.
escape
(
currentUsername
)}
`
);
$tempNote
.
find
(
'
.note-headline-light
'
)
.
text
(
`@
${
_
.
escape
(
currentUsername
)}
`
);
return
$tempNote
;
return
$tempNote
;
}
}
...
@@ -1481,7 +1703,7 @@ export default class Notes {
...
@@ -1481,7 +1703,7 @@ export default class Notes {
<i>
${
formContent
}
</i>
<i>
${
formContent
}
</i>
</div>
</div>
</div>
</div>
</li>`
</li>`
,
);
);
return
$tempNote
;
return
$tempNote
;
...
@@ -1513,11 +1735,22 @@ export default class Notes {
...
@@ -1513,11 +1735,22 @@ export default class Notes {
const
$submitBtn
=
$
(
e
.
target
);
const
$submitBtn
=
$
(
e
.
target
);
let
$form
=
$submitBtn
.
parents
(
'
form
'
);
let
$form
=
$submitBtn
.
parents
(
'
form
'
);
const
$closeBtn
=
$form
.
find
(
'
.js-note-target-close
'
);
const
$closeBtn
=
$form
.
find
(
'
.js-note-target-close
'
);
const
isDiscussionNote
=
$submitBtn
.
parent
().
find
(
'
li.droplab-item-selected
'
).
attr
(
'
id
'
)
===
'
discussion
'
;
const
isDiscussionNote
=
$submitBtn
.
parent
()
.
find
(
'
li.droplab-item-selected
'
)
.
attr
(
'
id
'
)
===
'
discussion
'
;
const
isMainForm
=
$form
.
hasClass
(
'
js-main-target-form
'
);
const
isMainForm
=
$form
.
hasClass
(
'
js-main-target-form
'
);
const
isDiscussionForm
=
$form
.
hasClass
(
'
js-discussion-note-form
'
);
const
isDiscussionForm
=
$form
.
hasClass
(
'
js-discussion-note-form
'
);
const
isDiscussionResolve
=
$submitBtn
.
hasClass
(
'
js-comment-resolve-button
'
);
const
isDiscussionResolve
=
$submitBtn
.
hasClass
(
const
{
formData
,
formContent
,
formAction
,
formContentOriginal
}
=
this
.
getFormData
(
$form
);
'
js-comment-resolve-button
'
,
);
const
{
formData
,
formContent
,
formAction
,
formContentOriginal
,
}
=
this
.
getFormData
(
$form
);
let
noteUniqueId
;
let
noteUniqueId
;
let
systemNoteUniqueId
;
let
systemNoteUniqueId
;
let
hasQuickActions
=
false
;
let
hasQuickActions
=
false
;
...
@@ -1547,23 +1780,30 @@ export default class Notes {
...
@@ -1547,23 +1780,30 @@ export default class Notes {
// Show placeholder note
// Show placeholder note
if
(
tempFormContent
)
{
if
(
tempFormContent
)
{
noteUniqueId
=
_
.
uniqueId
(
'
tempNote_
'
);
noteUniqueId
=
_
.
uniqueId
(
'
tempNote_
'
);
$notesContainer
.
append
(
this
.
createPlaceholderNote
({
$notesContainer
.
append
(
formContent
:
tempFormContent
,
this
.
createPlaceholderNote
({
uniqueId
:
noteUniqueId
,
formContent
:
tempFormContent
,
isDiscussionNote
,
uniqueId
:
noteUniqueId
,
currentUsername
:
gon
.
current_username
,
isDiscussionNote
,
currentUserFullname
:
gon
.
current_user_fullname
,
currentUsername
:
gon
.
current_username
,
currentUserAvatar
:
gon
.
current_user_avatar_url
,
currentUserFullname
:
gon
.
current_user_fullname
,
}));
currentUserAvatar
:
gon
.
current_user_avatar_url
,
}),
);
}
}
// Show placeholder system note
// Show placeholder system note
if
(
hasQuickActions
)
{
if
(
hasQuickActions
)
{
systemNoteUniqueId
=
_
.
uniqueId
(
'
tempSystemNote_
'
);
systemNoteUniqueId
=
_
.
uniqueId
(
'
tempSystemNote_
'
);
$notesContainer
.
append
(
this
.
createPlaceholderSystemNote
({
$notesContainer
.
append
(
formContent
:
this
.
getQuickActionDescription
(
formContent
,
AjaxCache
.
get
(
gl
.
GfmAutoComplete
.
dataSources
.
commands
)),
this
.
createPlaceholderSystemNote
({
uniqueId
:
systemNoteUniqueId
,
formContent
:
this
.
getQuickActionDescription
(
}));
formContent
,
AjaxCache
.
get
(
gl
.
GfmAutoComplete
.
dataSources
.
commands
),
),
uniqueId
:
systemNoteUniqueId
,
}),
);
}
}
// Clear the form textarea
// Clear the form textarea
...
@@ -1577,8 +1817,9 @@ export default class Notes {
...
@@ -1577,8 +1817,9 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
// Make request to submit comment on server
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
axios
.
then
((
res
)
=>
{
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
(
res
=>
{
const
note
=
res
.
data
;
const
note
=
res
.
data
;
// Submission successful! remove placeholder
// Submission successful! remove placeholder
...
@@ -1595,7 +1836,9 @@ export default class Notes {
...
@@ -1595,7 +1836,9 @@ export default class Notes {
// Reset cached commands list when command is applied
// Reset cached commands list when command is applied
if
(
hasQuickActions
)
{
if
(
hasQuickActions
)
{
$form
.
find
(
'
textarea.js-note-text
'
).
trigger
(
'
clear-commands-cache.atwho
'
);
$form
.
find
(
'
textarea.js-note-text
'
)
.
trigger
(
'
clear-commands-cache.atwho
'
);
}
}
// Clear previous form errors
// Clear previous form errors
...
@@ -1640,11 +1883,14 @@ export default class Notes {
...
@@ -1640,11 +1883,14 @@ export default class Notes {
// append flash-container to the Notes list
// append flash-container to the Notes list
if
(
$notesContainer
.
length
)
{
if
(
$notesContainer
.
length
)
{
$notesContainer
.
append
(
'
<div class="flash-container" style="display: none;"></div>
'
);
$notesContainer
.
append
(
'
<div class="flash-container" style="display: none;"></div>
'
,
);
}
}
Notes
.
refreshVueNotes
();
Notes
.
refreshVueNotes
();
}
else
if
(
isMainForm
)
{
// Check if this was main thread comment
}
else
if
(
isMainForm
)
{
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
// Show final note element on UI and perform form and action buttons cleanup
this
.
addNote
(
$form
,
note
);
this
.
addNote
(
$form
,
note
);
this
.
reenableTargetFormSubmitButton
(
e
);
this
.
reenableTargetFormSubmitButton
(
e
);
...
@@ -1655,7 +1901,8 @@ export default class Notes {
...
@@ -1655,7 +1901,8 @@ export default class Notes {
}
}
$form
.
trigger
(
'
ajax:success
'
,
[
note
]);
$form
.
trigger
(
'
ajax:success
'
,
[
note
]);
}).
catch
(()
=>
{
})
.
catch
(()
=>
{
// Submission failed, remove placeholder note and show Flash error message
// Submission failed, remove placeholder note and show Flash error message
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
...
@@ -1675,7 +1922,9 @@ export default class Notes {
...
@@ -1675,7 +1922,9 @@ export default class Notes {
// Show form again on UI on failure
// Show form again on UI on failure
if
(
isDiscussionForm
&&
$notesContainer
.
length
)
{
if
(
isDiscussionForm
&&
$notesContainer
.
length
)
{
const
replyButton
=
$notesContainer
.
parent
().
find
(
'
.js-discussion-reply-button
'
);
const
replyButton
=
$notesContainer
.
parent
()
.
find
(
'
.js-discussion-reply-button
'
);
this
.
replyToDiscussionNote
(
replyButton
[
0
]);
this
.
replyToDiscussionNote
(
replyButton
[
0
]);
$form
=
$notesContainer
.
parent
().
find
(
'
form
'
);
$form
=
$notesContainer
.
parent
().
find
(
'
form
'
);
}
}
...
@@ -1720,12 +1969,19 @@ export default class Notes {
...
@@ -1720,12 +1969,19 @@ export default class Notes {
// Show updated comment content temporarily
// Show updated comment content temporarily
$noteBodyText
.
html
(
formContent
);
$noteBodyText
.
html
(
formContent
);
$editingNote
.
removeClass
(
'
is-editing fade-in-full
'
).
addClass
(
'
being-posted fade-in-half
'
);
$editingNote
$editingNote
.
find
(
'
.note-headline-meta a
'
).
html
(
'
<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>
'
);
.
removeClass
(
'
is-editing fade-in-full
'
)
.
addClass
(
'
being-posted fade-in-half
'
);
$editingNote
.
find
(
'
.note-headline-meta a
'
)
.
html
(
'
<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>
'
,
);
/* eslint-disable promise/catch-or-return */
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
// Make request to update comment on server
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
(({
data
})
=>
{
.
then
(({
data
})
=>
{
// Submission successful! render final note element
// Submission successful! render final note element
this
.
updateNote
(
data
,
$editingNote
);
this
.
updateNote
(
data
,
$editingNote
);
...
...
app/assets/javascripts/notes/components/comment_form.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
$
from
'
jquery
'
;
import
$
from
'
jquery
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
_
from
'
underscore
'
;
import
_
from
'
underscore
'
;
import
Autosize
from
'
autosize
'
;
import
Autosize
from
'
autosize
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
Flash
from
'
../../flash
'
;
import
Flash
from
'
../../flash
'
;
import
Autosave
from
'
../../autosave
'
;
import
Autosave
from
'
../../autosave
'
;
import
TaskList
from
'
../../task_list
'
;
import
TaskList
from
'
../../task_list
'
;
import
{
capitalizeFirstCharacter
,
convertToCamelCase
}
from
'
../../lib/utils/text_utility
'
;
import
{
import
*
as
constants
from
'
../constants
'
;
capitalizeFirstCharacter
,
import
eventHub
from
'
../event_hub
'
;
convertToCamelCase
,
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
}
from
'
../../lib/utils/text_utility
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
*
as
constants
from
'
../constants
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
loadingButton
from
'
../../vue_shared/components/loading_button.vue
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
discussionLockedWidget
from
'
./discussion_locked_widget.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
import
loadingButton
from
'
../../vue_shared/components/loading_button.vue
'
;
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
discussionLockedWidget
from
'
./discussion_locked_widget.vue
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
export
default
{
export
default
{
name
:
'
CommentForm
'
,
name
:
'
CommentForm
'
,
components
:
{
components
:
{
issueWarning
,
issueWarning
,
noteSignedOutWidget
,
noteSignedOutWidget
,
discussionLockedWidget
,
discussionLockedWidget
,
markdownField
,
markdownField
,
userAvatarLink
,
userAvatarLink
,
loadingButton
,
loadingButton
,
},
mixins
:
[
issuableStateMixin
],
props
:
{
noteableType
:
{
type
:
String
,
required
:
true
,
},
},
mixins
:
[
},
issuableStateMixin
,
data
()
{
],
return
{
props
:
{
note
:
''
,
noteableType
:
{
noteType
:
constants
.
COMMENT
,
type
:
String
,
isSubmitting
:
false
,
required
:
true
,
isSubmitButtonDisabled
:
true
,
},
};
},
computed
:
{
...
mapGetters
([
'
getCurrentUserLastNote
'
,
'
getUserData
'
,
'
getNoteableData
'
,
'
getNotesData
'
,
'
openState
'
,
]),
...
mapState
([
'
isToggleStateButtonLoading
'
]),
noteableDisplayName
()
{
return
this
.
noteableType
.
replace
(
/_/g
,
'
'
);
},
},
data
()
{
isLoggedIn
()
{
return
{
return
this
.
getUserData
.
id
;
note
:
''
,
}
,
noteType
:
constants
.
COMMENT
,
commentButtonTitle
()
{
isSubmitting
:
false
,
return
this
.
noteType
===
constants
.
COMMENT
isSubmitButtonDisabled
:
true
,
?
'
Comment
'
}
;
:
'
Start discussion
'
;
},
},
computed
:
{
isOpen
()
{
...
mapGetters
([
return
(
'
getCurrentUserLastNote
'
,
this
.
openState
===
constants
.
OPENED
||
'
getUserData
'
,
this
.
openState
===
constants
.
REOPENED
'
getNoteableData
'
,
);
'
getNotesData
'
,
},
'
openState
'
,
canCreateNote
()
{
]),
return
this
.
getNoteableData
.
current_user
.
can_create_note
;
...
mapState
([
},
'
isToggleStateButtonLoading
'
,
issueActionButtonTitle
()
{
]),
const
openOrClose
=
this
.
isOpen
?
'
close
'
:
'
reopen
'
;
noteableDisplayName
()
{
return
this
.
noteableType
.
replace
(
/_/g
,
'
'
);
},
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
commentButtonTitle
()
{
return
this
.
noteType
===
constants
.
COMMENT
?
'
Comment
'
:
'
Start discussion
'
;
},
isOpen
()
{
return
this
.
openState
===
constants
.
OPENED
||
this
.
openState
===
constants
.
REOPENED
;
},
canCreateNote
()
{
return
this
.
getNoteableData
.
current_user
.
can_create_note
;
},
issueActionButtonTitle
()
{
const
openOrClose
=
this
.
isOpen
?
'
close
'
:
'
reopen
'
;
if
(
this
.
note
.
length
)
{
if
(
this
.
note
.
length
)
{
return
sprintf
(
return
sprintf
(
__
(
'
%{actionText} & %{openOrClose} %{noteable}
'
),
{
__
(
'
%{actionText} & %{openOrClose} %{noteable}
'
),
actionText
:
this
.
commentButtonTitle
,
{
openOrClose
,
actionText
:
this
.
commentButtonTitle
,
noteable
:
this
.
noteableDisplayName
,
openOrClose
,
});
noteable
:
this
.
noteableDisplayName
,
}
},
);
}
return
sprintf
(
return
sprintf
(
__
(
'
%{openOrClose} %{noteable}
'
),
{
__
(
'
%{openOrClose} %{noteable}
'
),
openOrClose
:
capitalizeFirstCharacter
(
openOrClose
),
{
noteable
:
this
.
noteableDisplayName
,
openOrClose
:
capitalizeFirstCharacter
(
openOrClose
),
});
noteable
:
this
.
noteableDisplayName
,
},
);
},
actionButtonClassNames
()
{
return
{
'
btn-reopen
'
:
!
this
.
isOpen
,
'
btn-close
'
:
this
.
isOpen
,
'
js-note-target-close
'
:
this
.
isOpen
,
'
js-note-target-reopen
'
:
!
this
.
isOpen
,
};
},
markdownDocsPath
()
{
return
this
.
getNotesData
.
markdownDocsPath
;
},
quickActionsDocsPath
()
{
return
this
.
getNotesData
.
quickActionsDocsPath
;
},
markdownPreviewPath
()
{
return
this
.
getNoteableData
.
preview_note_path
;
},
author
()
{
return
this
.
getUserData
;
},
canUpdateIssue
()
{
return
this
.
getNoteableData
.
current_user
.
can_update
;
},
endpoint
()
{
return
this
.
getNoteableData
.
create_note_path
;
},
},
},
watch
:
{
actionButtonClassNames
()
{
note
(
newNote
)
{
return
{
this
.
setIsSubmitButtonDisabled
(
newNote
,
this
.
isSubmitting
);
'
btn-reopen
'
:
!
this
.
isOpen
,
}
,
'
btn-close
'
:
this
.
isOpen
,
isSubmitting
(
newValue
)
{
'
js-note-target-close
'
:
this
.
isOpen
,
this
.
setIsSubmitButtonDisabled
(
this
.
note
,
newValue
);
'
js-note-target-reopen
'
:
!
this
.
isOpen
,
}
,
}
;
},
},
mounted
()
{
markdownDocsPath
()
{
// jQuery is needed here because it is a custom event being dispatched with jQuery.
return
this
.
getNotesData
.
markdownDocsPath
;
$
(
document
).
on
(
'
issuable:change
'
,
(
e
,
isClosed
)
=>
{
},
this
.
toggleIssueLocalState
(
isClosed
?
constants
.
CLOSED
:
constants
.
REOPENED
);
quickActionsDocsPath
()
{
});
return
this
.
getNotesData
.
quickActionsDocsPath
;
},
markdownPreviewPath
()
{
return
this
.
getNoteableData
.
preview_note_path
;
},
author
()
{
return
this
.
getUserData
;
},
canUpdateIssue
()
{
return
this
.
getNoteableData
.
current_user
.
can_update
;
},
endpoint
()
{
return
this
.
getNoteableData
.
create_note_path
;
},
},
watch
:
{
note
(
newNote
)
{
this
.
setIsSubmitButtonDisabled
(
newNote
,
this
.
isSubmitting
);
},
isSubmitting
(
newValue
)
{
this
.
setIsSubmitButtonDisabled
(
this
.
note
,
newValue
);
},
},
mounted
()
{
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$
(
document
).
on
(
'
issuable:change
'
,
(
e
,
isClosed
)
=>
{
this
.
toggleIssueLocalState
(
isClosed
?
constants
.
CLOSED
:
constants
.
REOPENED
,
);
});
this
.
initAutoSave
();
this
.
initAutoSave
();
this
.
initTaskList
();
this
.
initTaskList
();
},
methods
:
{
...
mapActions
([
'
saveNote
'
,
'
stopPolling
'
,
'
restartPolling
'
,
'
removePlaceholderNotes
'
,
'
closeIssue
'
,
'
reopenIssue
'
,
'
toggleIssueLocalState
'
,
'
toggleStateButtonLoading
'
,
]),
setIsSubmitButtonDisabled
(
note
,
isSubmitting
)
{
if
(
!
_
.
isEmpty
(
note
)
&&
!
isSubmitting
)
{
this
.
isSubmitButtonDisabled
=
false
;
}
else
{
this
.
isSubmitButtonDisabled
=
true
;
}
},
},
methods
:
{
handleSave
(
withIssueAction
)
{
...
mapActions
([
this
.
isSubmitting
=
true
;
'
saveNote
'
,
'
stopPolling
'
,
'
restartPolling
'
,
'
removePlaceholderNotes
'
,
'
closeIssue
'
,
'
reopenIssue
'
,
'
toggleIssueLocalState
'
,
'
toggleStateButtonLoading
'
,
]),
setIsSubmitButtonDisabled
(
note
,
isSubmitting
)
{
if
(
!
_
.
isEmpty
(
note
)
&&
!
isSubmitting
)
{
this
.
isSubmitButtonDisabled
=
false
;
}
else
{
this
.
isSubmitButtonDisabled
=
true
;
}
},
handleSave
(
withIssueAction
)
{
this
.
isSubmitting
=
true
;
if
(
this
.
note
.
length
)
{
if
(
this
.
note
.
length
)
{
const
noteData
=
{
const
noteData
=
{
endpoint
:
this
.
endpoint
,
endpoint
:
this
.
endpoint
,
flashContainer
:
this
.
$el
,
flashContainer
:
this
.
$el
,
data
:
{
data
:
{
note
:
{
note
:
{
noteable_type
:
this
.
noteableType
,
noteable_type
:
this
.
noteableType
,
noteable_id
:
this
.
getNoteableData
.
id
,
noteable_id
:
this
.
getNoteableData
.
id
,
note
:
this
.
note
,
note
:
this
.
note
,
},
},
},
};
},
};
if
(
this
.
noteType
===
constants
.
DISCUSSION
)
{
if
(
this
.
noteType
===
constants
.
DISCUSSION
)
{
noteData
.
data
.
note
.
type
=
constants
.
DISCUSSION_NOTE
;
noteData
.
data
.
note
.
type
=
constants
.
DISCUSSION_NOTE
;
}
}
this
.
note
=
''
;
// Empty textarea while being requested. Repopulate in catch
this
.
note
=
''
;
// Empty textarea while being requested. Repopulate in catch
this
.
resizeTextarea
();
this
.
resizeTextarea
();
this
.
stopPolling
();
this
.
stopPolling
();
this
.
saveNote
(
noteData
)
this
.
saveNote
(
noteData
)
.
then
((
res
)
=>
{
.
then
(
res
=>
{
this
.
enableButton
();
this
.
enableButton
();
this
.
restartPolling
();
this
.
restartPolling
();
if
(
res
.
errors
)
{
if
(
res
.
errors
)
{
if
(
res
.
errors
.
commands_only
)
{
if
(
res
.
errors
.
commands_only
)
{
this
.
discard
();
}
else
{
Flash
(
'
Something went wrong while adding your comment. Please try again.
'
,
'
alert
'
,
this
.
$refs
.
commentForm
,
);
}
}
else
{
this
.
discard
();
this
.
discard
();
}
else
{
Flash
(
'
Something went wrong while adding your comment. Please try again.
'
,
'
alert
'
,
this
.
$refs
.
commentForm
,
);
}
}
}
else
{
this
.
discard
();
}
if
(
withIssueAction
)
{
if
(
withIssueAction
)
{
this
.
toggleIssueState
();
this
.
toggleIssueState
();
}
}
})
})
.
catch
(()
=>
{
.
catch
(()
=>
{
this
.
enableButton
();
this
.
enableButton
();
this
.
discard
(
false
);
this
.
discard
(
false
);
const
msg
=
const
msg
=
`Your comment could not be submitted!
`Your comment could not be submitted!
Please check your network connection and try again.`
;
Please check your network connection and try again.`
;
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
note
=
noteData
.
data
.
note
.
note
;
// Restore textarea content.
this
.
note
=
noteData
.
data
.
note
.
note
;
// Restore textarea content.
this
.
removePlaceholderNotes
();
this
.
removePlaceholderNotes
();
});
});
}
else
{
}
else
{
this
.
toggleIssueState
();
this
.
toggleIssueState
();
}
}
},
},
enableButton
()
{
enableButton
()
{
this
.
isSubmitting
=
false
;
this
.
isSubmitting
=
false
;
},
},
toggleIssueState
()
{
toggleIssueState
()
{
if
(
this
.
isOpen
)
{
if
(
this
.
isOpen
)
{
this
.
closeIssue
()
this
.
closeIssue
()
.
then
(()
=>
this
.
enableButton
())
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
.
catch
(()
=>
{
this
.
enableButton
();
this
.
enableButton
();
this
.
toggleStateButtonLoading
(
false
);
this
.
toggleStateButtonLoading
(
false
);
Flash
(
Flash
(
sprintf
(
sprintf
(
__
(
'
Something went wrong while closing the %{issuable}. Please try again later
'
),
__
(
{
issuable
:
this
.
noteableDisplayName
}
,
'
Something went wrong while closing the %{issuable}. Please try again later
'
,
),
),
);
{
issuable
:
this
.
noteableDisplayName
},
});
),
}
else
{
);
this
.
reopenIssue
()
});
.
then
(()
=>
this
.
enableButton
())
}
else
{
.
catch
(()
=>
{
this
.
reopenIssue
()
this
.
enableButton
();
.
then
(()
=>
this
.
enableButton
())
this
.
toggleStateButtonLoading
(
false
);
.
catch
(()
=>
{
Flash
(
this
.
enableButton
();
sprintf
(
this
.
toggleStateButtonLoading
(
false
);
__
(
'
Something went wrong while reopening the %{issuable}. Please try again later
'
),
Flash
(
{
issuable
:
this
.
noteableDisplayName
},
sprintf
(
__
(
'
Something went wrong while reopening the %{issuable}. Please try again later
'
,
),
),
);
{
issuable
:
this
.
noteableDisplayName
},
});
),
}
);
},
});
discard
(
shouldClear
=
true
)
{
}
// `blur` is needed to clear slash commands autocomplete cache if event fired.
},
// `focus` is needed to remain cursor in the textarea.
discard
(
shouldClear
=
true
)
{
this
.
$refs
.
textarea
.
blur
();
// `blur` is needed to clear slash commands autocomplete cache if event fired.
this
.
$refs
.
textarea
.
focus
();
// `focus` is needed to remain cursor in the textarea.
this
.
$refs
.
textarea
.
blur
();
this
.
$refs
.
textarea
.
focus
();
if
(
shouldClear
)
{
if
(
shouldClear
)
{
this
.
note
=
''
;
this
.
note
=
''
;
this
.
resizeTextarea
();
this
.
resizeTextarea
();
this
.
$refs
.
markdownField
.
previewMarkdown
=
false
;
this
.
$refs
.
markdownField
.
previewMarkdown
=
false
;
}
}
this
.
autosave
.
reset
();
this
.
autosave
.
reset
();
},
},
setNoteType
(
type
)
{
setNoteType
(
type
)
{
this
.
noteType
=
type
;
this
.
noteType
=
type
;
},
},
editCurrentUserLastNote
()
{
editCurrentUserLastNote
()
{
if
(
this
.
note
===
''
)
{
if
(
this
.
note
===
''
)
{
const
lastNote
=
this
.
getCurrentUserLastNote
;
const
lastNote
=
this
.
getCurrentUserLastNote
;
if
(
lastNote
)
{
if
(
lastNote
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
noteId
:
lastNote
.
id
,
noteId
:
lastNote
.
id
,
});
});
}
}
}
},
}
initAutoSave
()
{
},
if
(
this
.
isLoggedIn
)
{
initAutoSave
()
{
const
noteableType
=
capitalizeFirstCharacter
(
convertToCamelCase
(
this
.
noteableType
));
if
(
this
.
isLoggedIn
)
{
const
noteableType
=
capitalizeFirstCharacter
(
convertToCamelCase
(
this
.
noteableType
),
);
this
.
autosave
=
new
Autosave
(
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
textarea
),
[
$
(
this
.
$refs
.
textarea
)
,
'
Note
'
,
[
'
Note
'
,
noteableType
,
this
.
getNoteableData
.
id
]
,
noteableType
,
);
this
.
getNoteableData
.
id
,
}
]);
}
,
}
initTaskList
()
{
},
return
new
TaskList
(
{
initTaskList
()
{
dataType
:
'
note
'
,
return
new
TaskList
({
fieldNam
e
:
'
note
'
,
dataTyp
e
:
'
note
'
,
selector
:
'
.notes
'
,
fieldName
:
'
note
'
,
});
selector
:
'
.notes
'
,
}
,
}
);
resizeTextarea
()
{
},
this
.
$nextTick
(()
=>
{
resizeTextarea
()
{
Autosize
.
update
(
this
.
$refs
.
textarea
);
this
.
$nextTick
(()
=>
{
}
);
Autosize
.
update
(
this
.
$refs
.
textarea
);
}
,
}
);
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/diff_file_header.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
export
default
{
components
:
{
components
:
{
ClipboardButton
,
ClipboardButton
,
Icon
,
Icon
,
},
props
:
{
diffFile
:
{
type
:
Object
,
required
:
true
,
},
},
props
:
{
},
diffFile
:
{
computed
:
{
type
:
Object
,
titleTag
()
{
required
:
true
,
return
this
.
diffFile
.
discussionPath
?
'
a
'
:
'
span
'
;
},
},
},
computed
:
{
},
titleTag
()
{
};
return
this
.
diffFile
.
discussionPath
?
'
a
'
:
'
span
'
;
},
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/diff_with_note.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
$
from
'
jquery
'
;
import
$
from
'
jquery
'
;
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
imageDiffHelper
from
'
~/image_diff/helpers/index
'
;
import
imageDiffHelper
from
'
~/image_diff/helpers/index
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
DiffFileHeader
from
'
./diff_file_header.vue
'
;
import
DiffFileHeader
from
'
./diff_file_header.vue
'
;
export
default
{
export
default
{
components
:
{
components
:
{
DiffFileHeader
,
DiffFileHeader
,
},
props
:
{
discussion
:
{
type
:
Object
,
required
:
true
,
},
},
props
:
{
},
discussion
:
{
computed
:
{
type
:
Object
,
isImageDiff
()
{
required
:
true
,
return
!
this
.
diffFile
.
text
;
},
},
},
computed
:
{
diffFileClass
()
{
isImageDiff
()
{
const
{
text
}
=
this
.
diffFile
;
return
!
this
.
diffFile
.
text
;
return
text
?
'
text-file
'
:
'
js-image-file
'
;
},
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
()
{
diffRows
()
{
if
(
this
.
isImageDiff
)
{
return
$
(
this
.
discussion
.
truncatedDiffLines
);
const
canCreateNote
=
false
;
const
renderCommentBadge
=
true
;
imageDiffHelper
.
initImageDiff
(
this
.
$refs
.
fileHolder
,
canCreateNote
,
renderCommentBadge
);
}
else
{
const
fileHolder
=
$
(
this
.
$refs
.
fileHolder
);
this
.
$nextTick
(()
=>
{
syntaxHighlight
(
fileHolder
);
});
}
},
},
methods
:
{
diffFile
()
{
rowTag
(
html
)
{
return
convertObjectPropsToCamelCase
(
this
.
discussion
.
diffFile
);
return
html
.
outerHTML
?
'
tr
'
:
'
template
'
;
},
},
},
};
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
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/discussion_counter.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
mapGetters
}
from
'
vuex
'
;
import
resolveSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolveSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
resolvedSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
mrIssueSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
mrIssueSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionSvg
from
'
icons/_next_discussion.svg
'
;
import
nextDiscussionSvg
from
'
icons/_next_discussion.svg
'
;
import
{
pluralize
}
from
'
../../lib/utils/text_utility
'
;
import
{
pluralize
}
from
'
../../lib/utils/text_utility
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
export
default
{
export
default
{
directives
:
{
directives
:
{
tooltip
,
tooltip
,
},
computed
:
{
...
mapGetters
([
'
getUserData
'
,
'
getNoteableData
'
,
'
discussionCount
'
,
'
unresolvedDiscussions
'
,
'
resolvedDiscussionCount
'
,
]),
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
},
computed
:
{
hasNextButton
()
{
...
mapGetters
([
return
this
.
isLoggedIn
&&
!
this
.
allResolved
;
'
getUserData
'
,
},
'
getNoteableData
'
,
countText
()
{
'
discussionCount
'
,
return
pluralize
(
'
discussion
'
,
this
.
discussionCount
);
'
unresolvedDiscussions
'
,
},
'
resolvedDiscussionCount
'
,
allResolved
()
{
]),
return
this
.
resolvedDiscussionCount
===
this
.
discussionCount
;
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
()
{
resolveAllDiscussionsIssuePath
()
{
this
.
resolveSvg
=
resolveSvg
;
return
this
.
getNoteableData
.
create_issue_to_resolve_discussions_path
;
this
.
resolvedSvg
=
resolvedSvg
;
},
this
.
mrIssueSvg
=
mrIssueSvg
;
firstUnresolvedDiscussionId
()
{
this
.
nextDiscussionSvg
=
nextDiscussionSvg
;
const
item
=
this
.
unresolvedDiscussions
[
0
]
||
{};
return
item
.
id
;
},
},
methods
:
{
},
jumpToFirstDiscussion
()
{
created
()
{
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
this
.
firstUnresolvedDiscussionId
}
"]`
);
this
.
resolveSvg
=
resolveSvg
;
const
activeTab
=
window
.
mrTabs
.
currentAction
;
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
'
)
{
if
(
activeTab
===
'
commits
'
||
activeTab
===
'
pipelines
'
)
{
window
.
mrTabs
.
activateTab
(
'
show
'
);
window
.
mrTabs
.
activateTab
(
'
show
'
);
}
}
if
(
el
)
{
if
(
el
)
{
scrollToElement
(
el
);
scrollToElement
(
el
);
}
}
},
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/discussion_locked_widget.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Issuable
from
'
~/vue_shared/mixins/issuable
'
;
import
Issuable
from
'
~/vue_shared/mixins/issuable
'
;
export
default
{
export
default
{
components
:
{
components
:
{
Icon
,
Icon
,
},
},
mixins
:
[
mixins
:
[
Issuable
],
Issuable
,
};
],
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_actions.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
mapGetters
}
from
'
vuex
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
editSvg
from
'
icons/_icon_pencil.svg
'
;
import
editSvg
from
'
icons/_icon_pencil.svg
'
;
import
resolveDiscussionSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolveDiscussionSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedDiscussionSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
resolvedDiscussionSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
ellipsisSvg
from
'
icons/_ellipsis_v.svg
'
;
import
ellipsisSvg
from
'
icons/_ellipsis_v.svg
'
;
import
loadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
loadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
export
default
{
export
default
{
name
:
'
NoteActions
'
,
name
:
'
NoteActions
'
,
directives
:
{
directives
:
{
tooltip
,
tooltip
,
},
},
components
:
{
components
:
{
loadingIcon
,
loadingIcon
,
},
},
props
:
{
props
:
{
authorId
:
{
authorId
:
{
type
:
Number
,
type
:
Number
,
required
:
true
,
required
:
true
,
},
},
noteId
:
{
noteId
:
{
type
:
Number
,
type
:
Number
,
required
:
true
,
required
:
true
,
},
},
accessLevel
:
{
accessLevel
:
{
type
:
String
,
type
:
String
,
required
:
false
,
required
:
false
,
default
:
''
,
default
:
''
,
},
},
reportAbusePath
:
{
reportAbusePath
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
},
canEdit
:
{
canEdit
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
true
,
required
:
true
,
},
},
canDelete
:
{
canDelete
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
true
,
required
:
true
,
},
},
resolvable
:
{
resolvable
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
false
,
required
:
false
,
default
:
false
,
default
:
false
,
},
},
isResolved
:
{
isResolved
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
false
,
required
:
false
,
default
:
false
,
default
:
false
,
},
},
isResolving
:
{
isResolving
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
false
,
required
:
false
,
default
:
false
,
default
:
false
,
},
},
resolvedBy
:
{
resolvedBy
:
{
type
:
Object
,
type
:
Object
,
required
:
false
,
required
:
false
,
default
:
()
=>
({}),
default
:
()
=>
({}),
},
},
canReportAsAbuse
:
{
canReportAsAbuse
:
{
type
:
Boolean
,
type
:
Boolean
,
required
:
true
,
required
:
true
,
},
},
},
},
computed
:
{
computed
:
{
...
mapGetters
([
...
mapGetters
([
'
getUserDataByProp
'
]),
'
getUserDataByProp
'
,
shouldShowActionsDropdown
()
{
]),
return
this
.
currentUserId
&&
(
this
.
canEdit
||
this
.
canReportAsAbuse
);
shouldShowActionsDropdown
()
{
},
return
this
.
currentUserId
&&
(
this
.
canEdit
||
this
.
canReportAsAbuse
);
canAddAwardEmoji
()
{
},
return
this
.
currentUserId
;
canAddAwardEmoji
()
{
},
return
this
.
currentUserId
;
isAuthoredByCurrentUser
()
{
},
return
this
.
authorId
===
this
.
currentUserId
;
isAuthoredByCurrentUser
()
{
},
return
this
.
authorId
===
this
.
currentUserId
;
currentUserId
()
{
},
return
this
.
getUserDataByProp
(
'
id
'
);
currentUserId
()
{
},
return
this
.
getUserDataByProp
(
'
id
'
);
resolveButtonTitle
()
{
},
let
title
=
'
Mark as resolved
'
;
resolveButtonTitle
()
{
let
title
=
'
Mark as resolved
'
;
if
(
this
.
resolvedBy
)
{
if
(
this
.
resolvedBy
)
{
title
=
`Resolved by
${
this
.
resolvedBy
.
name
}
`
;
title
=
`Resolved by
${
this
.
resolvedBy
.
name
}
`
;
}
}
return
title
;
return
title
;
},
},
},
},
created
()
{
created
()
{
this
.
emojiSmiling
=
emojiSmiling
;
this
.
emojiSmiling
=
emojiSmiling
;
this
.
emojiSmile
=
emojiSmile
;
this
.
emojiSmile
=
emojiSmile
;
this
.
emojiSmiley
=
emojiSmiley
;
this
.
emojiSmiley
=
emojiSmiley
;
this
.
editSvg
=
editSvg
;
this
.
editSvg
=
editSvg
;
this
.
ellipsisSvg
=
ellipsisSvg
;
this
.
ellipsisSvg
=
ellipsisSvg
;
this
.
resolveDiscussionSvg
=
resolveDiscussionSvg
;
this
.
resolveDiscussionSvg
=
resolveDiscussionSvg
;
this
.
resolvedDiscussionSvg
=
resolvedDiscussionSvg
;
this
.
resolvedDiscussionSvg
=
resolvedDiscussionSvg
;
},
},
methods
:
{
methods
:
{
onEdit
()
{
onEdit
()
{
this
.
$emit
(
'
handleEdit
'
);
this
.
$emit
(
'
handleEdit
'
);
},
},
onDelete
()
{
onDelete
()
{
this
.
$emit
(
'
handleDelete
'
);
this
.
$emit
(
'
handleDelete
'
);
},
},
onResolve
()
{
onResolve
()
{
this
.
$emit
(
'
handleResolve
'
);
this
.
$emit
(
'
handleResolve
'
);
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_attachment.vue
View file @
fdc9ae2e
<
script
>
<
script
>
export
default
{
export
default
{
name
:
'
NoteAttachment
'
,
name
:
'
NoteAttachment
'
,
props
:
{
props
:
{
attachment
:
{
attachment
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_awards_list.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
Flash
from
'
../../flash
'
;
import
Flash
from
'
../../flash
'
;
import
{
glEmojiTag
}
from
'
../../emoji
'
;
import
{
glEmojiTag
}
from
'
../../emoji
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
export
default
{
export
default
{
directives
:
{
directives
:
{
tooltip
,
tooltip
,
},
props
:
{
awards
:
{
type
:
Array
,
required
:
true
,
},
},
props
:
{
toggleAwardPath
:
{
awards
:
{
type
:
String
,
type
:
Array
,
required
:
true
,
required
:
true
,
},
toggleAwardPath
:
{
type
:
String
,
required
:
true
,
},
noteAuthorId
:
{
type
:
Number
,
required
:
true
,
},
noteId
:
{
type
:
Number
,
required
:
true
,
},
},
},
computed
:
{
noteAuthorId
:
{
...
mapGetters
([
type
:
Number
,
'
getUserData
'
,
required
:
true
,
]),
// `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
// [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
// This method will group emojis by their name as an Object. See below.
// {
// foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ],
// bar: [ { name: bar, user: user1 } ]
// }
// We need to do this otherwise we will render the same emoji over and over again.
groupedAwards
()
{
const
awards
=
this
.
awards
.
reduce
((
acc
,
award
)
=>
{
if
(
Object
.
prototype
.
hasOwnProperty
.
call
(
acc
,
award
.
name
))
{
acc
[
award
.
name
].
push
(
award
);
}
else
{
Object
.
assign
(
acc
,
{
[
award
.
name
]:
[
award
]
});
}
return
acc
;
},
{});
const
orderedAwards
=
{};
const
{
thumbsdown
,
thumbsup
}
=
awards
;
// Always show thumbsup and thumbsdown first
if
(
thumbsup
)
{
orderedAwards
.
thumbsup
=
thumbsup
;
delete
awards
.
thumbsup
;
}
if
(
thumbsdown
)
{
orderedAwards
.
thumbsdown
=
thumbsdown
;
delete
awards
.
thumbsdown
;
}
return
Object
.
assign
({},
orderedAwards
,
awards
);
},
isAuthoredByMe
()
{
return
this
.
noteAuthorId
===
this
.
getUserData
.
id
;
},
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
},
},
created
()
{
noteId
:
{
this
.
emojiSmiling
=
emojiSmiling
;
type
:
Number
,
this
.
emojiSmile
=
emojiSmile
;
required
:
true
,
this
.
emojiSmiley
=
emojiSmiley
;
},
},
methods
:
{
},
...
mapActions
([
computed
:
{
'
toggleAwardRequest
'
,
...
mapGetters
([
'
getUserData
'
]),
]),
// `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
getAwardHTML
(
name
)
{
// [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
return
glEmojiTag
(
name
);
// This method will group emojis by their name as an Object. See below.
},
// {
getAwardClassBindings
(
awardList
,
awardName
)
{
// foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ],
return
{
// bar: [ { name: bar, user: user1 } ]
active
:
this
.
hasReactionByCurrentUser
(
awardList
),
// }
disabled
:
!
this
.
canInteractWithEmoji
(
awardList
,
awardName
),
// We need to do this otherwise we will render the same emoji over and over again.
};
groupedAwards
()
{
},
const
awards
=
this
.
awards
.
reduce
((
acc
,
award
)
=>
{
canInteractWithEmoji
(
awardList
,
awardName
)
{
if
(
Object
.
prototype
.
hasOwnProperty
.
call
(
acc
,
award
.
name
))
{
let
isAllowed
=
true
;
acc
[
award
.
name
].
push
(
award
);
const
restrictedEmojis
=
[
'
thumbsup
'
,
'
thumbsdown
'
];
}
else
{
Object
.
assign
(
acc
,
{
[
award
.
name
]:
[
award
]
});
// Users can not add :+1: and :-1: to their own notes
if
(
this
.
getUserData
.
id
===
this
.
noteAuthorId
&&
restrictedEmojis
.
indexOf
(
awardName
)
>
-
1
)
{
isAllowed
=
false
;
}
return
this
.
getUserData
.
id
&&
isAllowed
;
},
hasReactionByCurrentUser
(
awardList
)
{
return
awardList
.
filter
(
award
=>
award
.
user
.
id
===
this
.
getUserData
.
id
).
length
;
},
awardTitle
(
awardsList
)
{
const
hasReactionByCurrentUser
=
this
.
hasReactionByCurrentUser
(
awardsList
);
const
TOOLTIP_NAME_COUNT
=
hasReactionByCurrentUser
?
9
:
10
;
let
awardList
=
awardsList
;
// Filter myself from list if I am awarded.
if
(
hasReactionByCurrentUser
)
{
awardList
=
awardList
.
filter
(
award
=>
award
.
user
.
id
!==
this
.
getUserData
.
id
);
}
// Get only 9-10 usernames to show in tooltip text.
const
namesToShow
=
awardList
.
slice
(
0
,
TOOLTIP_NAME_COUNT
).
map
(
award
=>
award
.
user
.
name
);
// Get the remaining list to use in `and x more` text.
const
remainingAwardList
=
awardList
.
slice
(
TOOLTIP_NAME_COUNT
,
awardList
.
length
);
// Add myself to the begining of the list so title will start with You.
if
(
hasReactionByCurrentUser
)
{
namesToShow
.
unshift
(
'
You
'
);
}
let
title
=
''
;
// We have 10+ awarded user, join them with comma and add `and x more`.
if
(
remainingAwardList
.
length
)
{
title
=
`
${
namesToShow
.
join
(
'
,
'
)}
, and
${
remainingAwardList
.
length
}
more.`
;
}
else
if
(
namesToShow
.
length
>
1
)
{
// Join all names with comma but not the last one, it will be added with and text.
title
=
namesToShow
.
slice
(
0
,
namesToShow
.
length
-
1
).
join
(
'
,
'
);
// If we have more than 2 users we need an extra comma before and text.
title
+=
namesToShow
.
length
>
2
?
'
,
'
:
''
;
title
+=
` and
${
namesToShow
.
slice
(
-
1
)}
`
;
// Append and text
}
else
{
// We have only 2 users so join them with and.
title
=
namesToShow
.
join
(
'
and
'
);
}
return
title
;
},
handleAward
(
awardName
)
{
if
(
!
this
.
isLoggedIn
)
{
return
;
}
let
parsedName
;
// 100 and 1234 emoji are a number. Callback for v-for click sends it as a string
switch
(
awardName
)
{
case
'
100
'
:
parsedName
=
100
;
break
;
case
'
1234
'
:
parsedName
=
1234
;
break
;
default
:
parsedName
=
awardName
;
break
;
}
}
const
data
=
{
return
acc
;
endpoint
:
this
.
toggleAwardPath
,
},
{});
noteId
:
this
.
noteId
,
awardName
:
parsedName
,
const
orderedAwards
=
{};
};
const
{
thumbsdown
,
thumbsup
}
=
awards
;
// Always show thumbsup and thumbsdown first
this
.
toggleAwardRequest
(
data
)
if
(
thumbsup
)
{
.
catch
(()
=>
Flash
(
'
Something went wrong on our end.
'
));
orderedAwards
.
thumbsup
=
thumbsup
;
},
delete
awards
.
thumbsup
;
}
if
(
thumbsdown
)
{
orderedAwards
.
thumbsdown
=
thumbsdown
;
delete
awards
.
thumbsdown
;
}
return
Object
.
assign
({},
orderedAwards
,
awards
);
},
isAuthoredByMe
()
{
return
this
.
noteAuthorId
===
this
.
getUserData
.
id
;
},
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
},
created
()
{
this
.
emojiSmiling
=
emojiSmiling
;
this
.
emojiSmile
=
emojiSmile
;
this
.
emojiSmiley
=
emojiSmiley
;
},
methods
:
{
...
mapActions
([
'
toggleAwardRequest
'
]),
getAwardHTML
(
name
)
{
return
glEmojiTag
(
name
);
},
getAwardClassBindings
(
awardList
,
awardName
)
{
return
{
active
:
this
.
hasReactionByCurrentUser
(
awardList
),
disabled
:
!
this
.
canInteractWithEmoji
(
awardList
,
awardName
),
};
},
canInteractWithEmoji
(
awardList
,
awardName
)
{
let
isAllowed
=
true
;
const
restrictedEmojis
=
[
'
thumbsup
'
,
'
thumbsdown
'
];
// Users can not add :+1: and :-1: to their own notes
if
(
this
.
getUserData
.
id
===
this
.
noteAuthorId
&&
restrictedEmojis
.
indexOf
(
awardName
)
>
-
1
)
{
isAllowed
=
false
;
}
return
this
.
getUserData
.
id
&&
isAllowed
;
},
hasReactionByCurrentUser
(
awardList
)
{
return
awardList
.
filter
(
award
=>
award
.
user
.
id
===
this
.
getUserData
.
id
)
.
length
;
},
awardTitle
(
awardsList
)
{
const
hasReactionByCurrentUser
=
this
.
hasReactionByCurrentUser
(
awardsList
,
);
const
TOOLTIP_NAME_COUNT
=
hasReactionByCurrentUser
?
9
:
10
;
let
awardList
=
awardsList
;
// Filter myself from list if I am awarded.
if
(
hasReactionByCurrentUser
)
{
awardList
=
awardList
.
filter
(
award
=>
award
.
user
.
id
!==
this
.
getUserData
.
id
,
);
}
// Get only 9-10 usernames to show in tooltip text.
const
namesToShow
=
awardList
.
slice
(
0
,
TOOLTIP_NAME_COUNT
)
.
map
(
award
=>
award
.
user
.
name
);
// Get the remaining list to use in `and x more` text.
const
remainingAwardList
=
awardList
.
slice
(
TOOLTIP_NAME_COUNT
,
awardList
.
length
,
);
// Add myself to the begining of the list so title will start with You.
if
(
hasReactionByCurrentUser
)
{
namesToShow
.
unshift
(
'
You
'
);
}
let
title
=
''
;
// We have 10+ awarded user, join them with comma and add `and x more`.
if
(
remainingAwardList
.
length
)
{
title
=
`
${
namesToShow
.
join
(
'
,
'
)}
, and
${
remainingAwardList
.
length
}
more.`
;
}
else
if
(
namesToShow
.
length
>
1
)
{
// Join all names with comma but not the last one, it will be added with and text.
title
=
namesToShow
.
slice
(
0
,
namesToShow
.
length
-
1
).
join
(
'
,
'
);
// If we have more than 2 users we need an extra comma before and text.
title
+=
namesToShow
.
length
>
2
?
'
,
'
:
''
;
title
+=
` and
${
namesToShow
.
slice
(
-
1
)}
`
;
// Append and text
}
else
{
// We have only 2 users so join them with and.
title
=
namesToShow
.
join
(
'
and
'
);
}
return
title
;
},
handleAward
(
awardName
)
{
if
(
!
this
.
isLoggedIn
)
{
return
;
}
let
parsedName
;
// 100 and 1234 emoji are a number. Callback for v-for click sends it as a string
switch
(
awardName
)
{
case
'
100
'
:
parsedName
=
100
;
break
;
case
'
1234
'
:
parsedName
=
1234
;
break
;
default
:
parsedName
=
awardName
;
break
;
}
const
data
=
{
endpoint
:
this
.
toggleAwardPath
,
noteId
:
this
.
noteId
,
awardName
:
parsedName
,
};
this
.
toggleAwardRequest
(
data
).
catch
(()
=>
Flash
(
'
Something went wrong on our end.
'
),
);
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_body.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
$
from
'
jquery
'
;
import
$
from
'
jquery
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteAwardsList
from
'
./note_awards_list.vue
'
;
import
noteAwardsList
from
'
./note_awards_list.vue
'
;
import
noteAttachment
from
'
./note_attachment.vue
'
;
import
noteAttachment
from
'
./note_attachment.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
TaskList
from
'
../../task_list
'
;
import
TaskList
from
'
../../task_list
'
;
import
autosave
from
'
../mixins/autosave
'
;
import
autosave
from
'
../mixins/autosave
'
;
export
default
{
export
default
{
components
:
{
components
:
{
noteEditedText
,
noteEditedText
,
noteAwardsList
,
noteAwardsList
,
noteAttachment
,
noteAttachment
,
noteForm
,
noteForm
,
},
mixins
:
[
autosave
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
mixins
:
[
canEdit
:
{
autosave
,
type
:
Boolean
,
],
required
:
true
,
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
canEdit
:
{
type
:
Boolean
,
required
:
true
,
},
isEditing
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
},
computed
:
{
isEditing
:
{
noteBody
()
{
type
:
Boolean
,
return
this
.
note
.
note
;
required
:
false
,
}
,
default
:
false
,
},
},
mounted
()
{
},
this
.
renderGFM
();
computed
:
{
this
.
initTaskList
();
noteBody
()
{
return
this
.
note
.
note
;
},
},
mounted
()
{
this
.
renderGFM
();
this
.
initTaskList
();
if
(
this
.
isEditing
)
{
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
}
},
updated
()
{
this
.
initTaskList
();
this
.
renderGFM
();
if
(
this
.
isEditing
)
{
if
(
this
.
isEditing
)
{
if
(
!
this
.
autosave
)
{
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
}
else
{
this
.
setAutoSave
();
}
}
}
},
methods
:
{
renderGFM
()
{
$
(
this
.
$refs
[
'
note-body
'
]).
renderGFM
();
},
},
updated
()
{
initTaskList
()
{
this
.
initTaskList
();
if
(
this
.
canEdit
)
{
this
.
renderGFM
();
this
.
taskList
=
new
TaskList
({
dataType
:
'
note
'
,
if
(
this
.
isEditing
)
{
fieldName
:
'
note
'
,
if
(
!
this
.
autosave
)
{
selector
:
'
.notes
'
,
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
});
}
else
{
this
.
setAutoSave
();
}
}
}
},
},
methods
:
{
handleFormUpdate
(
note
,
parentElement
,
callback
)
{
renderGFM
()
{
this
.
$emit
(
'
handleFormUpdate
'
,
note
,
parentElement
,
callback
);
$
(
this
.
$refs
[
'
note-body
'
]).
renderGFM
();
},
},
formCancelHandler
(
shouldConfirm
,
isDirty
)
{
initTaskList
()
{
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
isDirty
);
if
(
this
.
canEdit
)
{
this
.
taskList
=
new
TaskList
({
dataType
:
'
note
'
,
fieldName
:
'
note
'
,
selector
:
'
.notes
'
,
});
}
},
handleFormUpdate
(
note
,
parentElement
,
callback
)
{
this
.
$emit
(
'
handleFormUpdate
'
,
note
,
parentElement
,
callback
);
},
formCancelHandler
(
shouldConfirm
,
isDirty
)
{
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
isDirty
);
},
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_edited_text.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
export
default
{
export
default
{
name
:
'
EditedNoteText
'
,
name
:
'
EditedNoteText
'
,
components
:
{
components
:
{
timeAgoTooltip
,
timeAgoTooltip
,
},
props
:
{
actionText
:
{
type
:
String
,
required
:
true
,
},
},
props
:
{
editedAt
:
{
actionText
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
editedAt
:
{
type
:
String
,
required
:
true
,
},
editedBy
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
className
:
{
type
:
String
,
required
:
false
,
default
:
'
edited-text
'
,
},
},
},
};
editedBy
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
className
:
{
type
:
String
,
required
:
false
,
default
:
'
edited-text
'
,
},
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_form.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
export
default
{
export
default
{
name
:
'
IssueNoteForm
'
,
name
:
'
IssueNoteForm
'
,
components
:
{
components
:
{
issueWarning
,
issueWarning
,
markdownField
,
markdownField
,
},
mixins
:
[
issuableStateMixin
,
resolvable
],
props
:
{
noteBody
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
},
mixins
:
[
noteId
:
{
issuableStateMixin
,
type
:
Number
,
resolvable
,
required
:
false
,
],
default
:
0
,
props
:
{
noteBody
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
noteId
:
{
type
:
Number
,
required
:
false
,
default
:
0
,
},
saveButtonTitle
:
{
type
:
String
,
required
:
false
,
default
:
'
Save comment
'
,
},
note
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
isEditing
:
{
type
:
Boolean
,
required
:
true
,
},
},
},
data
()
{
saveButtonTitle
:
{
return
{
type
:
String
,
updatedNoteBody
:
this
.
noteBody
,
required
:
false
,
conflictWhileEditing
:
false
,
default
:
'
Save comment
'
,
isSubmitting
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
};
},
},
computed
:
{
note
:
{
...
mapGetters
([
type
:
Object
,
'
getDiscussionLastNote
'
,
required
:
false
,
'
getNoteableData
'
,
default
:
()
=>
({}),
'
getNoteableDataByProp
'
,
'
getNotesDataByProp
'
,
'
getUserDataByProp
'
,
]),
noteHash
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
markdownPreviewPath
()
{
return
this
.
getNoteableDataByProp
(
'
preview_note_path
'
);
},
markdownDocsPath
()
{
return
this
.
getNotesDataByProp
(
'
markdownDocsPath
'
);
},
quickActionsDocsPath
()
{
return
!
this
.
isEditing
?
this
.
getNotesDataByProp
(
'
quickActionsDocsPath
'
)
:
undefined
;
},
currentUserId
()
{
return
this
.
getUserDataByProp
(
'
id
'
);
},
isDisabled
()
{
return
!
this
.
updatedNoteBody
.
length
||
this
.
isSubmitting
;
},
},
},
watch
:
{
isEditing
:
{
noteBody
()
{
type
:
Boolean
,
if
(
this
.
updatedNoteBody
===
this
.
noteBody
)
{
required
:
true
,
this
.
updatedNoteBody
=
this
.
noteBody
;
},
}
else
{
},
this
.
conflictWhileEditing
=
true
;
data
()
{
}
return
{
},
updatedNoteBody
:
this
.
noteBody
,
conflictWhileEditing
:
false
,
isSubmitting
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
};
},
computed
:
{
...
mapGetters
([
'
getDiscussionLastNote
'
,
'
getNoteableData
'
,
'
getNoteableDataByProp
'
,
'
getNotesDataByProp
'
,
'
getUserDataByProp
'
,
]),
noteHash
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
markdownPreviewPath
()
{
return
this
.
getNoteableDataByProp
(
'
preview_note_path
'
);
},
markdownDocsPath
()
{
return
this
.
getNotesDataByProp
(
'
markdownDocsPath
'
);
},
quickActionsDocsPath
()
{
return
!
this
.
isEditing
?
this
.
getNotesDataByProp
(
'
quickActionsDocsPath
'
)
:
undefined
;
},
},
mounte
d
()
{
currentUserI
d
()
{
this
.
$refs
.
textarea
.
focus
(
);
return
this
.
getUserDataByProp
(
'
id
'
);
},
},
methods
:
{
isDisabled
()
{
...
mapActions
([
return
!
this
.
updatedNoteBody
.
length
||
this
.
isSubmitting
;
'
toggleResolveNote
'
,
},
]),
},
handleUpdate
(
shouldResolve
)
{
watch
:
{
const
beforeSubmitDiscussionState
=
this
.
discussionResolved
;
noteBody
()
{
this
.
isSubmitting
=
true
;
if
(
this
.
updatedNoteBody
===
this
.
noteBody
)
{
this
.
updatedNoteBody
=
this
.
noteBody
;
}
else
{
this
.
conflictWhileEditing
=
true
;
}
},
},
mounted
()
{
this
.
$refs
.
textarea
.
focus
();
},
methods
:
{
...
mapActions
([
'
toggleResolveNote
'
]),
handleUpdate
(
shouldResolve
)
{
const
beforeSubmitDiscussionState
=
this
.
discussionResolved
;
this
.
isSubmitting
=
true
;
this
.
$emit
(
'
handleFormUpdate
'
,
this
.
updatedNoteBody
,
this
.
$refs
.
editNoteForm
,
()
=>
{
this
.
$emit
(
'
handleFormUpdate
'
,
this
.
updatedNoteBody
,
this
.
$refs
.
editNoteForm
,
()
=>
{
this
.
isSubmitting
=
false
;
this
.
isSubmitting
=
false
;
if
(
shouldResolve
)
{
if
(
shouldResolve
)
{
this
.
resolveHandler
(
beforeSubmitDiscussionState
);
this
.
resolveHandler
(
beforeSubmitDiscussionState
);
}
}
});
},
},
);
editMyLastNote
()
{
},
if
(
this
.
updatedNoteBody
===
''
)
{
editMyLastNote
()
{
const
lastNoteInDiscussion
=
this
.
getDiscussionLastNote
(
this
.
updatedNoteBody
);
if
(
this
.
updatedNoteBody
===
''
)
{
const
lastNoteInDiscussion
=
this
.
getDiscussionLastNote
(
this
.
updatedNoteBody
,
);
if
(
lastNoteInDiscussion
)
{
if
(
lastNoteInDiscussion
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
noteId
:
lastNoteInDiscussion
.
id
,
noteId
:
lastNoteInDiscussion
.
id
,
});
});
}
}
}
},
}
cancelHandler
(
shouldConfirm
=
false
)
{
},
// Sends information about confirm message and if the textarea has changed
cancelHandler
(
shouldConfirm
=
false
)
{
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
this
.
noteBody
!==
this
.
updatedNoteBody
);
// Sends information about confirm message and if the textarea has changed
},
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
this
.
noteBody
!==
this
.
updatedNoteBody
,
);
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_header.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
{
mapActions
}
from
'
vuex
'
;
import
{
mapActions
}
from
'
vuex
'
;
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
export
default
{
export
default
{
components
:
{
components
:
{
timeAgoTooltip
,
timeAgoTooltip
,
},
props
:
{
author
:
{
type
:
Object
,
required
:
true
,
},
},
props
:
{
createdAt
:
{
author
:
{
type
:
String
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
createdAt
:
{
type
:
String
,
required
:
true
,
},
actionText
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
actionTextHtml
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
noteId
:
{
type
:
Number
,
required
:
true
,
},
includeToggle
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
expanded
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
},
},
computed
:
{
actionText
:
{
toggleChevronClass
()
{
type
:
String
,
return
this
.
expanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
required
:
false
,
},
default
:
''
,
noteTimestampLink
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
},
},
methods
:
{
actionTextHtml
:
{
...
mapActions
([
type
:
String
,
'
setTargetNoteHash
'
,
required
:
false
,
]),
default
:
''
,
handleToggle
()
{
this
.
$emit
(
'
toggleHandler
'
);
},
updateTargetNoteHash
()
{
this
.
setTargetNoteHash
(
this
.
noteTimestampLink
);
},
},
},
};
noteId
:
{
type
:
Number
,
required
:
true
,
},
includeToggle
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
expanded
:
{
type
:
Boolean
,
required
:
false
,
default
:
true
,
},
},
computed
:
{
toggleChevronClass
()
{
return
this
.
expanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
},
noteTimestampLink
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
},
methods
:
{
...
mapActions
([
'
setTargetNoteHash
'
]),
handleToggle
()
{
this
.
$emit
(
'
toggleHandler
'
);
},
updateTargetNoteHash
()
{
this
.
setTargetNoteHash
(
this
.
noteTimestampLink
);
},
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_signed_out_widget.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
mapGetters
}
from
'
vuex
'
;
export
default
{
export
default
{
computed
:
{
computed
:
{
...
mapGetters
([
...
mapGetters
([
'
getNotesDataByProp
'
]),
'
getNotesDataByProp
'
,
registerLink
()
{
]),
return
this
.
getNotesDataByProp
(
'
registerPath
'
);
registerLink
()
{
return
this
.
getNotesDataByProp
(
'
registerPath
'
);
},
signInLink
()
{
return
this
.
getNotesDataByProp
(
'
newSessionPath
'
);
},
},
},
};
signInLink
()
{
return
this
.
getNotesDataByProp
(
'
newSessionPath
'
);
},
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/noteable_discussion.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
resolveDiscussionsSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
resolveDiscussionsSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionsSvg
from
'
icons/_next_discussion.svg
'
;
import
nextDiscussionsSvg
from
'
icons/_next_discussion.svg
'
;
import
Flash
from
'
../../flash
'
;
import
Flash
from
'
../../flash
'
;
import
{
SYSTEM_NOTE
}
from
'
../constants
'
;
import
{
SYSTEM_NOTE
}
from
'
../constants
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
noteableNote
from
'
./noteable_note.vue
'
;
import
noteableNote
from
'
./noteable_note.vue
'
;
import
noteHeader
from
'
./note_header.vue
'
;
import
noteHeader
from
'
./note_header.vue
'
;
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
diffWithNote
from
'
./diff_with_note.vue
'
;
import
diffWithNote
from
'
./diff_with_note.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
autosave
from
'
../mixins/autosave
'
;
import
autosave
from
'
../mixins/autosave
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
export
default
{
export
default
{
components
:
{
components
:
{
noteableNote
,
noteableNote
,
diffWithNote
,
diffWithNote
,
userAvatarLink
,
userAvatarLink
,
noteHeader
,
noteHeader
,
noteSignedOutWidget
,
noteSignedOutWidget
,
noteEditedText
,
noteEditedText
,
noteForm
,
noteForm
,
placeholderNote
,
placeholderNote
,
placeholderSystemNote
,
placeholderSystemNote
,
},
directives
:
{
tooltip
,
},
mixins
:
[
autosave
,
noteable
,
resolvable
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
directives
:
{
},
tooltip
,
data
()
{
},
return
{
mixins
:
[
isReplying
:
false
,
autosave
,
isResolving
:
false
,
noteable
,
resolveAsThread
:
true
,
resolvable
,
};
],
},
props
:
{
computed
:
{
note
:
{
...
mapGetters
([
type
:
Object
,
'
getNoteableData
'
,
required
:
true
,
'
discussionCount
'
,
},
'
resolvedDiscussionCount
'
,
},
'
unresolvedDiscussions
'
,
data
()
{
]),
discussion
()
{
return
{
return
{
isReplying
:
false
,
...
this
.
note
.
notes
[
0
],
isResolving
:
false
,
truncatedDiffLines
:
this
.
note
.
truncated_diff_lines
,
resolveAsThread
:
true
,
diffFile
:
this
.
note
.
diff_file
,
diffDiscussion
:
this
.
note
.
diff_discussion
,
imageDiffHtml
:
this
.
note
.
image_diff_html
,
};
};
},
},
computed
:
{
author
()
{
...
mapGetters
([
return
this
.
discussion
.
author
;
'
getNoteableData
'
,
},
'
discussionCount
'
,
canReply
()
{
'
resolvedDiscussionCount
'
,
return
this
.
getNoteableData
.
current_user
.
can_create_note
;
'
unresolvedDiscussions
'
,
},
]),
newNotePath
()
{
discussion
()
{
return
this
.
getNoteableData
.
create_note_path
;
return
{
},
...
this
.
note
.
notes
[
0
],
lastUpdatedBy
()
{
truncatedDiffLines
:
this
.
note
.
truncated_diff_lines
,
const
{
notes
}
=
this
.
note
;
diffFile
:
this
.
note
.
diff_file
,
diffDiscussion
:
this
.
note
.
diff_discussion
,
imageDiffHtml
:
this
.
note
.
image_diff_html
,
};
},
author
()
{
return
this
.
discussion
.
author
;
},
canReply
()
{
return
this
.
getNoteableData
.
current_user
.
can_create_note
;
},
newNotePath
()
{
return
this
.
getNoteableData
.
create_note_path
;
},
lastUpdatedBy
()
{
const
{
notes
}
=
this
.
note
;
if
(
notes
.
length
>
1
)
{
if
(
notes
.
length
>
1
)
{
return
notes
[
notes
.
length
-
1
].
author
;
return
notes
[
notes
.
length
-
1
].
author
;
}
}
return
null
;
return
null
;
},
},
lastUpdatedAt
()
{
lastUpdatedAt
()
{
const
{
notes
}
=
this
.
note
;
const
{
notes
}
=
this
.
note
;
if
(
notes
.
length
>
1
)
{
if
(
notes
.
length
>
1
)
{
return
notes
[
notes
.
length
-
1
].
created_at
;
return
notes
[
notes
.
length
-
1
].
created_at
;
}
}
return
null
;
return
null
;
},
},
hasUnresolvedDiscussion
()
{
hasUnresolvedDiscussion
()
{
return
this
.
unresolvedDiscussions
.
length
>
0
;
return
this
.
unresolvedDiscussions
.
length
>
0
;
},
},
wrapperComponent
()
{
wrapperComponent
()
{
return
(
this
.
discussion
.
diffDiscussion
&&
this
.
discussion
.
diffFile
)
?
diffWithNote
:
'
div
'
;
return
this
.
discussion
.
diffDiscussion
&&
this
.
discussion
.
diffFile
},
?
diffWithNote
wrapperClass
()
{
:
'
div
'
;
return
this
.
isDiffDiscussion
?
''
:
'
panel panel-default
'
;
},
},
},
mounted
()
{
wrapperClass
()
{
if
(
this
.
isReplying
)
{
return
this
.
isDiffDiscussion
?
''
:
'
panel panel-default
'
;
},
},
mounted
()
{
if
(
this
.
isReplying
)
{
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
}
},
updated
()
{
if
(
this
.
isReplying
)
{
if
(
!
this
.
autosave
)
{
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
}
else
{
this
.
setAutoSave
();
}
}
},
}
updated
()
{
},
if
(
this
.
isReplying
)
{
created
()
{
if
(
!
this
.
autosave
)
{
this
.
resolveDiscussionsSvg
=
resolveDiscussionsSvg
;
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
this
.
nextDiscussionsSvg
=
nextDiscussionsSvg
;
}
else
{
},
this
.
setAutoSave
();
methods
:
{
...
mapActions
([
'
saveNote
'
,
'
toggleDiscussion
'
,
'
removePlaceholderNotes
'
,
'
toggleResolveNote
'
,
]),
componentName
(
note
)
{
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
}
}
return
placeholderNote
;
}
}
return
noteableNote
;
},
},
created
()
{
componentData
(
note
)
{
this
.
resolveDiscussionsSvg
=
resolveDiscussionsSvg
;
return
note
.
isPlaceholderNote
?
this
.
note
.
notes
[
0
]
:
note
;
this
.
nextDiscussionsSvg
=
nextDiscussionsSvg
;
},
},
methods
:
{
toggleDiscussionHandler
()
{
...
mapActions
([
this
.
toggleDiscussion
({
discussionId
:
this
.
note
.
id
});
'
saveNote
'
,
},
'
toggleDiscussion
'
,
showReplyForm
()
{
'
removePlaceholderNotes
'
,
this
.
isReplying
=
true
;
'
toggleResolveNote
'
,
},
]),
cancelReplyForm
(
shouldConfirm
)
{
componentName
(
note
)
{
if
(
shouldConfirm
&&
this
.
$refs
.
noteForm
.
isDirty
)
{
if
(
note
.
isPlaceholderNote
)
{
const
msg
=
'
Are you sure you want to cancel creating this comment?
'
;
if
(
note
.
placeholderType
===
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
}
return
placeholderNote
;
}
return
noteableNote
;
// eslint-disable-next-line no-alert
},
if
(
!
confirm
(
msg
))
{
componentData
(
note
)
{
return
;
return
note
.
isPlaceholderNote
?
this
.
note
.
notes
[
0
]
:
note
;
},
toggleDiscussionHandler
()
{
this
.
toggleDiscussion
({
discussionId
:
this
.
note
.
id
});
},
showReplyForm
()
{
this
.
isReplying
=
true
;
},
cancelReplyForm
(
shouldConfirm
)
{
if
(
shouldConfirm
&&
this
.
$refs
.
noteForm
.
isDirty
)
{
// eslint-disable-next-line no-alert
if
(
!
confirm
(
'
Are you sure you want to cancel creating this comment?
'
))
{
return
;
}
}
}
}
this
.
resetAutoSave
();
this
.
resetAutoSave
();
this
.
isReplying
=
false
;
this
.
isReplying
=
false
;
},
},
saveReply
(
noteText
,
form
,
callback
)
{
saveReply
(
noteText
,
form
,
callback
)
{
const
replyData
=
{
const
replyData
=
{
endpoint
:
this
.
newNotePath
,
endpoint
:
this
.
newNotePath
,
flashContainer
:
this
.
$el
,
flashContainer
:
this
.
$el
,
data
:
{
data
:
{
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
target_type
:
this
.
noteableType
,
target_type
:
this
.
noteableType
,
target_id
:
this
.
discussion
.
noteable_id
,
target_id
:
this
.
discussion
.
noteable_id
,
note
:
{
note
:
noteText
},
note
:
{
note
:
noteText
},
},
},
};
};
this
.
isReplying
=
false
;
this
.
isReplying
=
false
;
this
.
saveNote
(
replyData
)
this
.
saveNote
(
replyData
)
.
then
(()
=>
{
.
then
(()
=>
{
this
.
resetAutoSave
();
this
.
resetAutoSave
();
callback
();
callback
();
})
})
.
catch
((
err
)
=>
{
.
catch
(
err
=>
{
this
.
removePlaceholderNotes
();
this
.
removePlaceholderNotes
();
this
.
isReplying
=
true
;
this
.
isReplying
=
true
;
this
.
$nextTick
(()
=>
{
this
.
$nextTick
(()
=>
{
const
msg
=
`Your comment could not be submitted!
const
msg
=
`Your comment could not be submitted!
Please check your network connection and try again.`
;
Please check your network connection and try again.`
;
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
$refs
.
noteForm
.
note
=
noteText
;
this
.
$refs
.
noteForm
.
note
=
noteText
;
callback
(
err
);
callback
(
err
);
});
});
});
},
});
jumpToDiscussion
()
{
},
const
unresolvedIds
=
this
.
unresolvedDiscussions
.
map
(
d
=>
d
.
id
);
jumpToDiscussion
()
{
const
index
=
unresolvedIds
.
indexOf
(
this
.
note
.
id
);
const
unresolvedIds
=
this
.
unresolvedDiscussions
.
map
(
d
=>
d
.
id
);
const
index
=
unresolvedIds
.
indexOf
(
this
.
note
.
id
);
if
(
index
>=
0
&&
index
!==
unresolvedIds
.
length
)
{
if
(
index
>=
0
&&
index
!==
unresolvedIds
.
length
)
{
const
nextId
=
unresolvedIds
[
index
+
1
];
const
nextId
=
unresolvedIds
[
index
+
1
];
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
nextId
}
"]`
);
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
nextId
}
"]`
);
if
(
el
)
{
if
(
el
)
{
scrollToElement
(
el
);
scrollToElement
(
el
);
}
}
}
}
,
}
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/noteable_note.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
$
from
'
jquery
'
;
import
$
from
'
jquery
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
escape
}
from
'
underscore
'
;
import
{
escape
}
from
'
underscore
'
;
import
Flash
from
'
../../flash
'
;
import
Flash
from
'
../../flash
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
noteHeader
from
'
./note_header.vue
'
;
import
noteHeader
from
'
./note_header.vue
'
;
import
noteActions
from
'
./note_actions.vue
'
;
import
noteActions
from
'
./note_actions.vue
'
;
import
noteBody
from
'
./note_body.vue
'
;
import
noteBody
from
'
./note_body.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
export
default
{
export
default
{
components
:
{
components
:
{
userAvatarLink
,
userAvatarLink
,
noteHeader
,
noteHeader
,
noteActions
,
noteActions
,
noteBody
,
noteBody
,
},
mixins
:
[
noteable
,
resolvable
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
mixins
:
[
},
noteable
,
data
()
{
resolvable
,
return
{
],
isEditing
:
false
,
props
:
{
isDeleting
:
false
,
note
:
{
isRequesting
:
false
,
type
:
Object
,
isResolving
:
false
,
required
:
true
,
};
},
},
computed
:
{
...
mapGetters
([
'
targetNoteHash
'
,
'
getUserData
'
]),
author
()
{
return
this
.
note
.
author
;
},
},
data
()
{
classNameBindings
()
{
return
{
return
{
isEditing
:
false
,
'
is-editing
'
:
this
.
isEditing
&&
!
this
.
isRequesting
,
isDeleting
:
false
,
'
is-requesting being-posted
'
:
this
.
isRequesting
,
isRequesting
:
false
,
'
disabled-content
'
:
this
.
isDeleting
,
isResolving
:
false
,
target
:
this
.
targetNoteHash
===
this
.
noteAnchorId
,
};
};
},
},
computed
:
{
canReportAsAbuse
()
{
...
mapGetters
([
return
(
'
targetNoteHash
'
,
this
.
note
.
report_abuse_path
&&
this
.
author
.
id
!==
this
.
getUserData
.
id
'
getUserData
'
,
);
]),
author
()
{
return
this
.
note
.
author
;
},
classNameBindings
()
{
return
{
'
is-editing
'
:
this
.
isEditing
&&
!
this
.
isRequesting
,
'
is-requesting being-posted
'
:
this
.
isRequesting
,
'
disabled-content
'
:
this
.
isDeleting
,
target
:
this
.
targetNoteHash
===
this
.
noteAnchorId
,
};
},
canReportAsAbuse
()
{
return
this
.
note
.
report_abuse_path
&&
this
.
author
.
id
!==
this
.
getUserData
.
id
;
},
noteAnchorId
()
{
return
`note_
${
this
.
note
.
id
}
`
;
},
},
},
noteAnchorId
()
{
created
()
{
return
`note_
${
this
.
note
.
id
}
`
;
eventHub
.
$on
(
'
enterEditMode
'
,
({
noteId
})
=>
{
if
(
noteId
===
this
.
note
.
id
)
{
this
.
isEditing
=
true
;
this
.
scrollToNoteIfNeeded
(
$
(
this
.
$el
));
}
});
},
},
},
methods
:
{
created
()
{
...
mapActions
([
eventHub
.
$on
(
'
enterEditMode
'
,
({
noteId
})
=>
{
'
deleteNote
'
,
if
(
noteId
===
this
.
note
.
id
)
{
'
updateNote
'
,
'
toggleResolveNote
'
,
'
scrollToNoteIfNeeded
'
,
]),
editHandler
()
{
this
.
isEditing
=
true
;
this
.
isEditing
=
true
;
},
this
.
scrollToNoteIfNeeded
(
$
(
this
.
$el
));
deleteHandler
()
{
}
// eslint-disable-next-line no-alert
});
if
(
confirm
(
'
Are you sure you want to delete this comment?
'
))
{
},
this
.
isDeleting
=
true
;
this
.
deleteNote
(
this
.
note
)
methods
:
{
.
then
(()
=>
{
...
mapActions
([
this
.
isDeleting
=
false
;
'
deleteNote
'
,
})
'
updateNote
'
,
.
catch
(()
=>
{
'
toggleResolveNote
'
,
Flash
(
'
Something went wrong while deleting your note. Please try again.
'
);
'
scrollToNoteIfNeeded
'
,
this
.
isDeleting
=
false
;
]),
});
editHandler
()
{
}
this
.
isEditing
=
true
;
},
},
formUpdateHandler
(
noteText
,
parentElement
,
callback
)
{
deleteHandler
()
{
const
data
=
{
// eslint-disable-next-line no-alert
endpoint
:
this
.
note
.
path
,
if
(
confirm
(
'
Are you sure you want to delete this comment?
'
))
{
note
:
{
this
.
isDeleting
=
true
;
target_type
:
this
.
noteableType
,
target_id
:
this
.
note
.
noteable_id
,
note
:
{
note
:
noteText
},
},
};
this
.
isRequesting
=
true
;
this
.
oldContent
=
this
.
note
.
note_html
;
this
.
note
.
note_html
=
escape
(
noteText
);
this
.
updateNote
(
data
)
this
.
deleteNote
(
this
.
note
)
.
then
(()
=>
{
.
then
(()
=>
{
this
.
isEditing
=
false
;
this
.
isDeleting
=
false
;
this
.
isRequesting
=
false
;
this
.
oldContent
=
null
;
$
(
this
.
$refs
.
noteBody
.
$el
).
renderGFM
();
this
.
$refs
.
noteBody
.
resetAutoSave
();
callback
();
})
})
.
catch
(()
=>
{
.
catch
(()
=>
{
this
.
isRequesting
=
false
;
Flash
(
this
.
isEditing
=
true
;
'
Something went wrong while deleting your note. Please try again.
'
,
this
.
$nextTick
(()
=>
{
);
const
msg
=
'
Something went wrong while editing your comment. Please try again.
'
;
this
.
isDeleting
=
false
;
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
recoverNoteContent
(
noteText
);
callback
();
});
});
});
},
}
formCancelHandler
(
shouldConfirm
,
isDirty
)
{
},
if
(
shouldConfirm
&&
isDirty
)
{
formUpdateHandler
(
noteText
,
parentElement
,
callback
)
{
// eslint-disable-next-line no-alert
const
data
=
{
if
(
!
confirm
(
'
Are you sure you want to cancel editing this comment?
'
))
return
;
endpoint
:
this
.
note
.
path
,
}
note
:
{
this
.
$refs
.
noteBody
.
resetAutoSave
();
target_type
:
this
.
noteableType
,
if
(
this
.
oldContent
)
{
target_id
:
this
.
note
.
noteable_id
,
this
.
note
.
note_html
=
this
.
oldContent
;
note
:
{
note
:
noteText
},
},
};
this
.
isRequesting
=
true
;
this
.
oldContent
=
this
.
note
.
note_html
;
this
.
note
.
note_html
=
escape
(
noteText
);
this
.
updateNote
(
data
)
.
then
(()
=>
{
this
.
isEditing
=
false
;
this
.
isRequesting
=
false
;
this
.
oldContent
=
null
;
this
.
oldContent
=
null
;
}
$
(
this
.
$refs
.
noteBody
.
$el
).
renderGFM
();
this
.
isEditing
=
false
;
this
.
$refs
.
noteBody
.
resetAutoSave
();
},
callback
();
recoverNoteContent
(
noteText
)
{
})
// we need to do this to prevent noteForm inconsistent content warning
.
catch
(()
=>
{
// this is something we intentionally do so we need to recover the content
this
.
isRequesting
=
false
;
this
.
note
.
note
=
noteText
;
this
.
isEditing
=
true
;
this
.
$refs
.
noteBody
.
$refs
.
noteForm
.
note
.
note
=
noteText
;
this
.
$nextTick
(()
=>
{
},
const
msg
=
'
Something went wrong while editing your comment. Please try again.
'
;
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
recoverNoteContent
(
noteText
);
callback
();
});
});
},
formCancelHandler
(
shouldConfirm
,
isDirty
)
{
if
(
shouldConfirm
&&
isDirty
)
{
// eslint-disable-next-line no-alert
if
(
!
confirm
(
'
Are you sure you want to cancel editing this comment?
'
))
return
;
}
this
.
$refs
.
noteBody
.
resetAutoSave
();
if
(
this
.
oldContent
)
{
this
.
note
.
note_html
=
this
.
oldContent
;
this
.
oldContent
=
null
;
}
this
.
isEditing
=
false
;
},
recoverNoteContent
(
noteText
)
{
// 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
.
note
=
noteText
;
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/notes_app.vue
View file @
fdc9ae2e
<
script
>
<
script
>
import
$
from
'
jquery
'
;
import
$
from
'
jquery
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
getLocationHash
}
from
'
../../lib/utils/url_utility
'
;
import
{
getLocationHash
}
from
'
../../lib/utils/url_utility
'
;
import
Flash
from
'
../../flash
'
;
import
Flash
from
'
../../flash
'
;
import
store
from
'
../stores/
'
;
import
store
from
'
../stores/
'
;
import
*
as
constants
from
'
../constants
'
;
import
*
as
constants
from
'
../constants
'
;
import
noteableNote
from
'
./noteable_note.vue
'
;
import
noteableNote
from
'
./noteable_note.vue
'
;
import
noteableDiscussion
from
'
./noteable_discussion.vue
'
;
import
noteableDiscussion
from
'
./noteable_discussion.vue
'
;
import
systemNote
from
'
../../vue_shared/components/notes/system_note.vue
'
;
import
systemNote
from
'
../../vue_shared/components/notes/system_note.vue
'
;
import
commentForm
from
'
./comment_form.vue
'
;
import
commentForm
from
'
./comment_form.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
skeletonLoadingContainer
from
'
../../vue_shared/components/notes/skeleton_note.vue
'
;
import
skeletonLoadingContainer
from
'
../../vue_shared/components/notes/skeleton_note.vue
'
;
export
default
{
export
default
{
name
:
'
NotesApp
'
,
name
:
'
NotesApp
'
,
components
:
{
components
:
{
noteableNote
,
noteableNote
,
noteableDiscussion
,
noteableDiscussion
,
systemNote
,
systemNote
,
commentForm
,
commentForm
,
loadingIcon
,
loadingIcon
,
placeholderNote
,
placeholderNote
,
placeholderSystemNote
,
placeholderSystemNote
,
},
props
:
{
noteableData
:
{
type
:
Object
,
required
:
true
,
},
},
props
:
{
notesData
:
{
noteableData
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
notesData
:
{
type
:
Object
,
required
:
true
,
},
userData
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
},
},
store
,
userData
:
{
data
()
{
type
:
Object
,
return
{
required
:
false
,
isLoading
:
true
,
default
:
()
=>
({}),
};
},
},
computed
:
{
},
...
mapGetters
([
store
,
'
notes
'
,
data
()
{
'
getNotesDataByProp
'
,
return
{
'
discussionCount
'
,
isLoading
:
true
,
]),
};
noteableType
()
{
},
// FIXME -- @fatihacet Get this from JSON data.
computed
:
{
const
{
ISSUE_NOTEABLE_TYPE
,
MERGE_REQUEST_NOTEABLE_TYPE
}
=
constants
;
...
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
;
return
this
.
noteableData
.
merge_params
},
?
MERGE_REQUEST_NOTEABLE_TYPE
allNotes
()
{
:
ISSUE_NOTEABLE_TYPE
;
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
);
this
.
setNoteableData
(
this
.
noteableData
);
this
.
setUserData
(
this
.
userData
);
},
},
mounted
()
{
allNotes
()
{
this
.
fetchNotes
();
if
(
this
.
isLoading
)
{
const
totalNotes
=
parseInt
(
this
.
notesData
.
totalNotes
,
10
)
||
0
;
const
parentElement
=
this
.
$el
.
parentElement
;
return
new
Array
(
totalNotes
).
fill
({
isSkeletonNote
:
true
,
if
(
parentElement
&&
parentElement
.
classList
.
contains
(
'
js-vue-notes-event
'
))
{
parentElement
.
addEventListener
(
'
toggleAward
'
,
(
event
)
=>
{
const
{
awardName
,
noteId
}
=
event
.
detail
;
this
.
actionToggleAward
({
awardName
,
noteId
});
});
});
}
}
document
.
addEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
return
this
.
notes
;
},
beforeDestroy
()
{
document
.
removeEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
},
},
methods
:
{
},
...
mapActions
({
created
()
{
actionFetchNotes
:
'
fetchNotes
'
,
this
.
setNotesData
(
this
.
notesData
);
poll
:
'
poll
'
,
this
.
setNoteableData
(
this
.
noteableData
);
actionToggleAward
:
'
toggleAward
'
,
this
.
setUserData
(
this
.
userData
);
scrollToNoteIfNeeded
:
'
scrollToNoteIfNeeded
'
,
},
setNotesData
:
'
setNotesData
'
,
mounted
()
{
setNoteableData
:
'
setNoteableData
'
,
this
.
fetchNotes
();
setUserData
:
'
setUserData
'
,
setLastFetchedAt
:
'
setLastFetchedAt
'
,
const
parentElement
=
this
.
$el
.
parentElement
;
setTargetNoteHash
:
'
setTargetNoteHash
'
,
}),
getComponentName
(
note
)
{
if
(
note
.
isSkeletonNote
)
{
return
skeletonLoadingContainer
;
}
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
constants
.
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
}
return
placeholderNote
;
}
else
if
(
note
.
individual_note
)
{
return
note
.
notes
[
0
].
system
?
systemNote
:
noteableNote
;
}
return
noteableDiscussion
;
if
(
},
parentElement
&&
getComponentData
(
note
)
{
parentElement
.
classList
.
contains
(
'
js-vue-notes-event
'
)
return
note
.
individual_note
?
note
.
notes
[
0
]
:
note
;
)
{
},
parentElement
.
addEventListener
(
'
toggleAward
'
,
event
=>
{
fetchNotes
()
{
const
{
awardName
,
noteId
}
=
event
.
detail
;
return
this
.
actionFetchNotes
(
this
.
getNotesDataByProp
(
'
discussionsPath
'
))
this
.
actionToggleAward
({
awardName
,
noteId
});
.
then
(()
=>
this
.
initPolling
())
});
.
then
(()
=>
{
}
this
.
isLoading
=
false
;
document
.
addEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
})
},
.
then
(()
=>
this
.
$nextTick
())
beforeDestroy
()
{
.
then
(()
=>
this
.
checkLocationHash
())
document
.
removeEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
.
catch
(()
=>
{
},
this
.
isLoading
=
false
;
methods
:
{
Flash
(
'
Something went wrong while fetching comments. Please try again.
'
);
...
mapActions
({
});
actionFetchNotes
:
'
fetchNotes
'
,
},
poll
:
'
poll
'
,
initPolling
()
{
actionToggleAward
:
'
toggleAward
'
,
if
(
this
.
isPollingInitialized
)
{
scrollToNoteIfNeeded
:
'
scrollToNoteIfNeeded
'
,
return
;
setNotesData
:
'
setNotesData
'
,
setNoteableData
:
'
setNoteableData
'
,
setUserData
:
'
setUserData
'
,
setLastFetchedAt
:
'
setLastFetchedAt
'
,
setTargetNoteHash
:
'
setTargetNoteHash
'
,
}),
getComponentName
(
note
)
{
if
(
note
.
isSkeletonNote
)
{
return
skeletonLoadingContainer
;
}
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
constants
.
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
}
}
return
placeholderNote
;
}
else
if
(
note
.
individual_note
)
{
return
note
.
notes
[
0
].
system
?
systemNote
:
noteableNote
;
}
this
.
setLastFetchedAt
(
this
.
getNotesDataByProp
(
'
lastFetchedAt
'
));
return
noteableDiscussion
;
},
getComponentData
(
note
)
{
return
note
.
individual_note
?
note
.
notes
[
0
]
:
note
;
},
fetchNotes
()
{
return
this
.
actionFetchNotes
(
this
.
getNotesDataByProp
(
'
discussionsPath
'
))
.
then
(()
=>
this
.
initPolling
())
.
then
(()
=>
{
this
.
isLoading
=
false
;
})
.
then
(()
=>
this
.
$nextTick
())
.
then
(()
=>
this
.
checkLocationHash
())
.
catch
(()
=>
{
this
.
isLoading
=
false
;
Flash
(
'
Something went wrong while fetching comments. Please try again.
'
,
);
});
},
initPolling
()
{
if
(
this
.
isPollingInitialized
)
{
return
;
}
this
.
poll
();
this
.
setLastFetchedAt
(
this
.
getNotesDataByProp
(
'
lastFetchedAt
'
));
this
.
isPollingInitialized
=
true
;
},
checkLocationHash
()
{
const
hash
=
getLocationHash
();
const
element
=
document
.
getElementById
(
hash
);
if
(
hash
&&
element
)
{
this
.
poll
();
this
.
setTargetNoteHash
(
hash
);
this
.
isPollingInitialized
=
true
;
this
.
scrollToNoteIfNeeded
(
$
(
element
));
},
}
checkLocationHash
()
{
},
const
hash
=
getLocationHash
();
const
element
=
document
.
getElementById
(
hash
);
if
(
hash
&&
element
)
{
this
.
setTargetNoteHash
(
hash
);
this
.
scrollToNoteIfNeeded
(
$
(
element
));
}
},
},
};
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/index.js
View file @
fdc9ae2e
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
notesApp
from
'
./components/notes_app.vue
'
;
import
notesApp
from
'
./components/notes_app.vue
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Vue
({
document
.
addEventListener
(
el
:
'
#js-vue-notes
'
,
'
DOMContentLoaded
'
,
components
:
{
()
=>
notesApp
,
new
Vue
({
},
el
:
'
#js-vue-notes
'
,
data
()
{
components
:
{
const
notesDataset
=
document
.
getElementById
(
'
js-vue-notes
'
).
dataset
;
notesApp
,
const
parsedUserData
=
JSON
.
parse
(
notesDataset
.
currentUserData
);
},
const
currentUserData
=
parsedUserData
?
{
data
()
{
id
:
parsedUserData
.
id
,
const
notesDataset
=
document
.
getElementById
(
'
js-vue-notes
'
).
dataset
;
name
:
parsedUserData
.
name
,
const
parsedUserData
=
JSON
.
parse
(
notesDataset
.
currentUserData
);
username
:
parsedUserData
.
username
,
let
currentUserData
=
{};
avatar_url
:
parsedUserData
.
avatar_path
||
parsedUserData
.
avatar_url
,
path
:
parsedUserData
.
path
,
if
(
parsedUserData
)
{
}
:
{};
currentUserData
=
{
id
:
parsedUserData
.
id
,
name
:
parsedUserData
.
name
,
username
:
parsedUserData
.
username
,
avatar_url
:
parsedUserData
.
avatar_path
||
parsedUserData
.
avatar_url
,
path
:
parsedUserData
.
path
,
};
}
return
{
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
,
currentUserData
,
notesData
:
JSON
.
parse
(
notesDataset
.
notesData
),
notesData
:
JSON
.
parse
(
notesDataset
.
notesData
),
};
};
},
},
render
(
createElement
)
{
render
(
createElement
)
{
return
createElement
(
'
notes-app
'
,
{
return
createElement
(
'
notes-app
'
,
{
props
:
{
props
:
{
noteableData
:
this
.
noteableData
,
noteableData
:
this
.
noteableData
,
notesData
:
this
.
notesData
,
notesData
:
this
.
notesData
,
userData
:
this
.
currentUserData
,
userData
:
this
.
currentUserData
,
},
});
},
},
});
}),
},
);
}));
app/assets/javascripts/notes/mixins/autosave.js
View file @
fdc9ae2e
...
@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
...
@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export
default
{
export
default
{
methods
:
{
methods
:
{
initAutoSave
(
noteableType
)
{
initAutoSave
(
noteableType
)
{
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
noteForm
.
$refs
.
textarea
),
[
'
Note
'
,
capitalizeFirstCharacter
(
noteableType
),
this
.
note
.
id
]);
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
noteForm
.
$refs
.
textarea
),
[
'
Note
'
,
capitalizeFirstCharacter
(
noteableType
),
this
.
note
.
id
,
]);
},
},
resetAutoSave
()
{
resetAutoSave
()
{
this
.
autosave
.
reset
();
this
.
autosave
.
reset
();
...
...
app/assets/javascripts/notes/mixins/resolvable.js
View file @
fdc9ae2e
...
@@ -12,7 +12,8 @@ export default {
...
@@ -12,7 +12,8 @@ export default {
discussionResolved
()
{
discussionResolved
()
{
const
{
notes
,
resolved
}
=
this
.
note
;
const
{
notes
,
resolved
}
=
this
.
note
;
if
(
notes
)
{
// Decide resolved state using store. Only valid for discussions.
if
(
notes
)
{
// Decide resolved state using store. Only valid for discussions.
return
notes
.
every
(
note
=>
note
.
resolved
&&
!
note
.
system
);
return
notes
.
every
(
note
=>
note
.
resolved
&&
!
note
.
system
);
}
}
...
@@ -26,7 +27,9 @@ export default {
...
@@ -26,7 +27,9 @@ export default {
return
__
(
'
Comment and resolve discussion
'
);
return
__
(
'
Comment and resolve discussion
'
);
}
}
return
this
.
discussionResolved
?
__
(
'
Unresolve discussion
'
)
:
__
(
'
Resolve discussion
'
);
return
this
.
discussionResolved
?
__
(
'
Unresolve discussion
'
)
:
__
(
'
Resolve discussion
'
);
},
},
},
},
methods
:
{
methods
:
{
...
@@ -42,7 +45,9 @@ export default {
...
@@ -42,7 +45,9 @@ export default {
})
})
.
catch
(()
=>
{
.
catch
(()
=>
{
this
.
isResolving
=
false
;
this
.
isResolving
=
false
;
const
msg
=
__
(
'
Something went wrong while resolving this discussion. Please try again.
'
);
const
msg
=
__
(
'
Something went wrong while resolving this discussion. Please try again.
'
,
);
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
});
});
},
},
...
...
app/assets/javascripts/notes/services/notes_service.js
View file @
fdc9ae2e
...
@@ -22,7 +22,9 @@ export default {
...
@@ -22,7 +22,9 @@ export default {
},
},
toggleResolveNote
(
endpoint
,
isResolved
)
{
toggleResolveNote
(
endpoint
,
isResolved
)
{
const
{
RESOLVE_NOTE_METHOD_NAME
,
UNRESOLVE_NOTE_METHOD_NAME
}
=
constants
;
const
{
RESOLVE_NOTE_METHOD_NAME
,
UNRESOLVE_NOTE_METHOD_NAME
}
=
constants
;
const
method
=
isResolved
?
UNRESOLVE_NOTE_METHOD_NAME
:
RESOLVE_NOTE_METHOD_NAME
;
const
method
=
isResolved
?
UNRESOLVE_NOTE_METHOD_NAME
:
RESOLVE_NOTE_METHOD_NAME
;
return
Vue
.
http
[
method
](
endpoint
);
return
Vue
.
http
[
method
](
endpoint
);
},
},
...
...
app/assets/javascripts/notes/stores/actions.js
View file @
fdc9ae2e
...
@@ -12,97 +12,115 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
...
@@ -12,97 +12,115 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let
eTagPoll
;
let
eTagPoll
;
export
const
setNotesData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_NOTES_DATA
,
data
);
export
const
setNotesData
=
({
commit
},
data
)
=>
export
const
setNoteableData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_NOTEABLE_DATA
,
data
);
commit
(
types
.
SET_NOTES_DATA
,
data
);
export
const
setUserData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_USER_DATA
,
data
);
export
const
setNoteableData
=
({
commit
},
data
)
=>
export
const
setLastFetchedAt
=
({
commit
},
data
)
=>
commit
(
types
.
SET_LAST_FETCHED_AT
,
data
);
commit
(
types
.
SET_NOTEABLE_DATA
,
data
);
export
const
setInitialNotes
=
({
commit
},
data
)
=>
commit
(
types
.
SET_INITIAL_NOTES
,
data
);
export
const
setUserData
=
({
commit
},
data
)
=>
export
const
setTargetNoteHash
=
({
commit
},
data
)
=>
commit
(
types
.
SET_TARGET_NOTE_HASH
,
data
);
commit
(
types
.
SET_USER_DATA
,
data
);
export
const
toggleDiscussion
=
({
commit
},
data
)
=>
commit
(
types
.
TOGGLE_DISCUSSION
,
data
);
export
const
setLastFetchedAt
=
({
commit
},
data
)
=>
commit
(
types
.
SET_LAST_FETCHED_AT
,
data
);
export
const
fetchNotes
=
({
commit
},
path
)
=>
service
export
const
setInitialNotes
=
({
commit
},
data
)
=>
.
fetchNotes
(
path
)
commit
(
types
.
SET_INITIAL_NOTES
,
data
);
.
then
(
res
=>
res
.
json
())
export
const
setTargetNoteHash
=
({
commit
},
data
)
=>
.
then
((
res
)
=>
{
commit
(
types
.
SET_TARGET_NOTE_HASH
,
data
);
commit
(
types
.
SET_INITIAL_NOTES
,
res
);
export
const
toggleDiscussion
=
({
commit
},
data
)
=>
});
commit
(
types
.
TOGGLE_DISCUSSION
,
data
);
export
const
fetchNotes
=
({
commit
},
path
)
=>
service
.
fetchNotes
(
path
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
{
commit
(
types
.
SET_INITIAL_NOTES
,
res
);
});
export
const
deleteNote
=
({
commit
},
note
)
=>
service
export
const
deleteNote
=
({
commit
},
note
)
=>
.
deleteNote
(
note
.
path
)
service
.
deleteNote
(
note
.
path
).
then
(()
=>
{
.
then
(()
=>
{
commit
(
types
.
DELETE_NOTE
,
note
);
commit
(
types
.
DELETE_NOTE
,
note
);
});
});
export
const
updateNote
=
({
commit
},
{
endpoint
,
note
})
=>
service
export
const
updateNote
=
({
commit
},
{
endpoint
,
note
})
=>
.
updateNote
(
endpoint
,
note
)
service
.
then
(
res
=>
res
.
json
())
.
updateNote
(
endpoint
,
note
)
.
then
((
res
)
=>
{
.
then
(
res
=>
res
.
json
())
commit
(
types
.
UPDATE_NOTE
,
res
);
.
then
(
res
=>
{
});
commit
(
types
.
UPDATE_NOTE
,
res
);
});
export
const
replyToDiscussion
=
({
commit
},
{
endpoint
,
data
})
=>
service
export
const
replyToDiscussion
=
({
commit
},
{
endpoint
,
data
})
=>
.
replyToDiscussion
(
endpoint
,
data
)
service
.
then
(
res
=>
res
.
json
())
.
replyToDiscussion
(
endpoint
,
data
)
.
then
((
res
)
=>
{
.
then
(
res
=>
res
.
json
())
commit
(
types
.
ADD_NEW_REPLY_TO_DISCUSSION
,
res
);
.
then
(
res
=>
{
commit
(
types
.
ADD_NEW_REPLY_TO_DISCUSSION
,
res
);
return
res
;
return
res
;
});
});
export
const
createNewNote
=
({
commit
},
{
endpoint
,
data
})
=>
service
export
const
createNewNote
=
({
commit
},
{
endpoint
,
data
})
=>
.
createNewNote
(
endpoint
,
data
)
service
.
then
(
res
=>
res
.
json
())
.
createNewNote
(
endpoint
,
data
)
.
then
((
res
)
=>
{
.
then
(
res
=>
res
.
json
())
if
(
!
res
.
errors
)
{
.
then
(
res
=>
{
commit
(
types
.
ADD_NEW_NOTE
,
res
);
if
(
!
res
.
errors
)
{
}
commit
(
types
.
ADD_NEW_NOTE
,
res
);
return
res
;
}
});
return
res
;
});
export
const
removePlaceholderNotes
=
({
commit
})
=>
export
const
removePlaceholderNotes
=
({
commit
})
=>
commit
(
types
.
REMOVE_PLACEHOLDER_NOTES
);
commit
(
types
.
REMOVE_PLACEHOLDER_NOTES
);
export
const
toggleResolveNote
=
({
commit
},
{
endpoint
,
isResolved
,
discussion
})
=>
service
export
const
toggleResolveNote
=
(
.
toggleResolveNote
(
endpoint
,
isResolved
)
{
commit
},
.
then
(
res
=>
res
.
json
())
{
endpoint
,
isResolved
,
discussion
},
.
then
((
res
)
=>
{
)
=>
const
mutationType
=
discussion
?
types
.
UPDATE_DISCUSSION
:
types
.
UPDATE_NOTE
;
service
.
toggleResolveNote
(
endpoint
,
isResolved
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
{
const
mutationType
=
discussion
?
types
.
UPDATE_DISCUSSION
:
types
.
UPDATE_NOTE
;
commit
(
mutationType
,
res
);
commit
(
mutationType
,
res
);
});
});
export
const
closeIssue
=
({
commit
,
dispatch
,
state
})
=>
{
export
const
closeIssue
=
({
commit
,
dispatch
,
state
})
=>
{
dispatch
(
'
toggleStateButtonLoading
'
,
true
);
dispatch
(
'
toggleStateButtonLoading
'
,
true
);
return
service
return
service
.
toggleIssueState
(
state
.
notesData
.
closePath
)
.
toggleIssueState
(
state
.
notesData
.
closePath
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
.
then
(
data
=>
{
commit
(
types
.
CLOSE_ISSUE
);
commit
(
types
.
CLOSE_ISSUE
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
dispatch
(
'
toggleStateButtonLoading
'
,
false
);
dispatch
(
'
toggleStateButtonLoading
'
,
false
);
});
});
};
};
export
const
reopenIssue
=
({
commit
,
dispatch
,
state
})
=>
{
export
const
reopenIssue
=
({
commit
,
dispatch
,
state
})
=>
{
dispatch
(
'
toggleStateButtonLoading
'
,
true
);
dispatch
(
'
toggleStateButtonLoading
'
,
true
);
return
service
return
service
.
toggleIssueState
(
state
.
notesData
.
reopenPath
)
.
toggleIssueState
(
state
.
notesData
.
reopenPath
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
.
then
(
data
=>
{
commit
(
types
.
REOPEN_ISSUE
);
commit
(
types
.
REOPEN_ISSUE
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
dispatch
(
'
toggleStateButtonLoading
'
,
false
);
dispatch
(
'
toggleStateButtonLoading
'
,
false
);
});
});
};
};
export
const
toggleStateButtonLoading
=
({
commit
},
value
)
=>
export
const
toggleStateButtonLoading
=
({
commit
},
value
)
=>
commit
(
types
.
TOGGLE_STATE_BUTTON_LOADING
,
value
);
commit
(
types
.
TOGGLE_STATE_BUTTON_LOADING
,
value
);
export
const
emitStateChangedEvent
=
({
commit
,
getters
},
data
)
=>
{
export
const
emitStateChangedEvent
=
({
commit
,
getters
},
data
)
=>
{
const
event
=
new
CustomEvent
(
'
issuable_vue_app:change
'
,
{
detail
:
{
const
event
=
new
CustomEvent
(
'
issuable_vue_app:change
'
,
{
data
,
detail
:
{
isClosed
:
getters
.
openState
===
constants
.
CLOSED
,
data
,
}
});
isClosed
:
getters
.
openState
===
constants
.
CLOSED
,
},
});
document
.
dispatchEvent
(
event
);
document
.
dispatchEvent
(
event
);
};
};
...
@@ -144,59 +162,70 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
...
@@ -144,59 +162,70 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
});
}
}
return
dispatch
(
methodToDispatch
,
noteData
)
return
dispatch
(
methodToDispatch
,
noteData
).
then
(
res
=>
{
.
then
((
res
)
=>
{
const
{
errors
}
=
res
;
const
{
errors
}
=
res
;
const
commandsChanges
=
res
.
commands_changes
;
const
commandsChanges
=
res
.
commands_changes
;
if
(
hasQuickActions
&&
errors
&&
Object
.
keys
(
errors
).
length
)
{
if
(
hasQuickActions
&&
errors
&&
Object
.
keys
(
errors
).
length
)
{
eTagPoll
.
makeRequest
();
eTagPoll
.
makeRequest
();
$
(
'
.js-gfm-input
'
).
trigger
(
'
clear-commands-cache.atwho
'
);
Flash
(
'
Commands applied
'
,
'
notice
'
,
noteData
.
flashContainer
);
}
if
(
commandsChanges
)
{
$
(
'
.js-gfm-input
'
).
trigger
(
'
clear-commands-cache.atwho
'
);
if
(
commandsChanges
.
emoji_award
)
{
Flash
(
'
Commands applied
'
,
'
notice
'
,
noteData
.
flashContainer
);
const
votesBlock
=
$
(
'
.js-awards-block
'
).
eq
(
0
);
}
loadAwardsHandler
()
.
then
((
awardsHandler
)
=>
{
awardsHandler
.
addAwardToEmojiBar
(
votesBlock
,
commandsChanges
.
emoji_award
);
awardsHandler
.
scrollToAwards
();
})
.
catch
(()
=>
{
Flash
(
'
Something went wrong while adding your award. Please try again.
'
,
'
alert
'
,
noteData
.
flashContainer
,
);
});
}
if
(
commandsChanges
.
spend_time
!=
null
||
commandsChanges
.
time_estimate
!=
null
)
{
if
(
commandsChanges
)
{
sidebarTimeTrackingEventHub
.
$emit
(
'
timeTrackingUpdated
'
,
res
);
if
(
commandsChanges
.
emoji_award
)
{
}
const
votesBlock
=
$
(
'
.js-awards-block
'
).
eq
(
0
);
loadAwardsHandler
()
.
then
(
awardsHandler
=>
{
awardsHandler
.
addAwardToEmojiBar
(
votesBlock
,
commandsChanges
.
emoji_award
,
);
awardsHandler
.
scrollToAwards
();
})
.
catch
(()
=>
{
Flash
(
'
Something went wrong while adding your award. Please try again.
'
,
'
alert
'
,
noteData
.
flashContainer
,
);
});
}
}
if
(
errors
&&
errors
.
commands_only
)
{
if
(
Flash
(
errors
.
commands_only
,
'
notice
'
,
noteData
.
flashContainer
);
commandsChanges
.
spend_time
!=
null
||
commandsChanges
.
time_estimate
!=
null
)
{
sidebarTimeTrackingEventHub
.
$emit
(
'
timeTrackingUpdated
'
,
res
);
}
}
commit
(
types
.
REMOVE_PLACEHOLDER_NOTES
);
}
return
res
;
if
(
errors
&&
errors
.
commands_only
)
{
});
Flash
(
errors
.
commands_only
,
'
notice
'
,
noteData
.
flashContainer
);
}
commit
(
types
.
REMOVE_PLACEHOLDER_NOTES
);
return
res
;
});
};
};
const
pollSuccessCallBack
=
(
resp
,
commit
,
state
,
getters
)
=>
{
const
pollSuccessCallBack
=
(
resp
,
commit
,
state
,
getters
)
=>
{
if
(
resp
.
notes
&&
resp
.
notes
.
length
)
{
if
(
resp
.
notes
&&
resp
.
notes
.
length
)
{
const
{
notesById
}
=
getters
;
const
{
notesById
}
=
getters
;
resp
.
notes
.
forEach
(
(
note
)
=>
{
resp
.
notes
.
forEach
(
note
=>
{
if
(
notesById
[
note
.
id
])
{
if
(
notesById
[
note
.
id
])
{
commit
(
types
.
UPDATE_NOTE
,
note
);
commit
(
types
.
UPDATE_NOTE
,
note
);
}
else
if
(
note
.
type
===
constants
.
DISCUSSION_NOTE
||
note
.
type
===
constants
.
DIFF_NOTE
)
{
}
else
if
(
const
discussion
=
utils
.
findNoteObjectById
(
state
.
notes
,
note
.
discussion_id
);
note
.
type
===
constants
.
DISCUSSION_NOTE
||
note
.
type
===
constants
.
DIFF_NOTE
)
{
const
discussion
=
utils
.
findNoteObjectById
(
state
.
notes
,
note
.
discussion_id
,
);
if
(
discussion
)
{
if
(
discussion
)
{
commit
(
types
.
ADD_NEW_REPLY_TO_DISCUSSION
,
note
);
commit
(
types
.
ADD_NEW_REPLY_TO_DISCUSSION
,
note
);
...
@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => {
...
@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => {
resource
:
service
,
resource
:
service
,
method
:
'
poll
'
,
method
:
'
poll
'
,
data
:
state
,
data
:
state
,
successCallback
:
resp
=>
resp
.
json
()
successCallback
:
resp
=>
.
then
(
data
=>
pollSuccessCallBack
(
data
,
commit
,
state
,
getters
)),
resp
errorCallback
:
()
=>
Flash
(
'
Something went wrong while fetching latest comments.
'
),
.
json
()
.
then
(
data
=>
pollSuccessCallBack
(
data
,
commit
,
state
,
getters
)),
errorCallback
:
()
=>
Flash
(
'
Something went wrong while fetching latest comments.
'
),
});
});
if
(
!
Visibility
.
hidden
())
{
if
(
!
Visibility
.
hidden
())
{
...
@@ -248,15 +280,22 @@ export const restartPolling = () => {
...
@@ -248,15 +280,22 @@ export const restartPolling = () => {
};
};
export
const
fetchData
=
({
commit
,
state
,
getters
})
=>
{
export
const
fetchData
=
({
commit
,
state
,
getters
})
=>
{
const
requestData
=
{
endpoint
:
state
.
notesData
.
notesPath
,
lastFetchedAt
:
state
.
lastFetchedAt
};
const
requestData
=
{
endpoint
:
state
.
notesData
.
notesPath
,
lastFetchedAt
:
state
.
lastFetchedAt
,
};
service
.
poll
(
requestData
)
service
.
poll
(
requestData
)
.
then
(
resp
=>
resp
.
json
)
.
then
(
resp
=>
resp
.
json
)
.
then
(
data
=>
pollSuccessCallBack
(
data
,
commit
,
state
,
getters
))
.
then
(
data
=>
pollSuccessCallBack
(
data
,
commit
,
state
,
getters
))
.
catch
(()
=>
Flash
(
'
Something went wrong while fetching latest comments.
'
));
.
catch
(()
=>
Flash
(
'
Something went wrong while fetching latest comments.
'
));
};
};
export
const
toggleAward
=
({
commit
,
state
,
getters
,
dispatch
},
{
awardName
,
noteId
})
=>
{
export
const
toggleAward
=
(
{
commit
,
state
,
getters
,
dispatch
},
{
awardName
,
noteId
},
)
=>
{
commit
(
types
.
TOGGLE_AWARD
,
{
awardName
,
note
:
getters
.
notesById
[
noteId
]
});
commit
(
types
.
TOGGLE_AWARD
,
{
awardName
,
note
:
getters
.
notesById
[
noteId
]
});
};
};
...
...
app/assets/javascripts/notes/stores/getters.js
View file @
fdc9ae2e
...
@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
...
@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export
const
openState
=
state
=>
state
.
noteableData
.
state
;
export
const
openState
=
state
=>
state
.
noteableData
.
state
;
export
const
getUserData
=
state
=>
state
.
userData
||
{};
export
const
getUserData
=
state
=>
state
.
userData
||
{};
export
const
getUserDataByProp
=
state
=>
prop
=>
state
.
userData
&&
state
.
userData
[
prop
];
export
const
getUserDataByProp
=
state
=>
prop
=>
state
.
userData
&&
state
.
userData
[
prop
];
export
const
notesById
=
state
=>
state
.
notes
.
reduce
((
acc
,
note
)
=>
{
export
const
notesById
=
state
=>
note
.
notes
.
every
(
n
=>
Object
.
assign
(
acc
,
{
[
n
.
id
]:
n
}));
state
.
notes
.
reduce
((
acc
,
note
)
=>
{
return
acc
;
note
.
notes
.
every
(
n
=>
Object
.
assign
(
acc
,
{
[
n
.
id
]:
n
}));
},
{});
return
acc
;
},
{});
const
reverseNotes
=
array
=>
array
.
slice
(
0
).
reverse
();
const
reverseNotes
=
array
=>
array
.
slice
(
0
).
reverse
();
const
isLastNote
=
(
note
,
state
)
=>
!
note
.
system
&&
const
isLastNote
=
(
note
,
state
)
=>
state
.
userData
&&
note
.
author
&&
!
note
.
system
&&
state
.
userData
&&
note
.
author
&&
note
.
author
.
id
===
state
.
userData
.
id
;
note
.
author
.
id
===
state
.
userData
.
id
;
export
const
getCurrentUserLastNote
=
state
=>
_
.
flatten
(
export
const
getCurrentUserLastNote
=
state
=>
reverseNotes
(
state
.
notes
)
_
.
flatten
(
.
map
(
note
=>
reverseNotes
(
note
.
notes
)),
reverseNotes
(
state
.
notes
)
.
map
(
note
=>
reverseNotes
(
note
.
notes
)),
).
find
(
el
=>
isLastNote
(
el
,
state
));
).
find
(
el
=>
isLastNote
(
el
,
state
));
export
const
getDiscussionLastNote
=
state
=>
discussion
=>
reverseNotes
(
discussion
.
notes
)
export
const
getDiscussionLastNote
=
state
=>
discussion
=>
.
find
(
el
=>
isLastNote
(
el
,
state
));
reverseNotes
(
discussion
.
notes
)
.
find
(
el
=>
isLastNote
(
el
,
state
));
export
const
discussionCount
=
(
state
)
=>
{
export
const
discussionCount
=
state
=>
{
const
discussions
=
state
.
notes
.
filter
(
n
=>
!
n
.
individual_note
);
const
discussions
=
state
.
notes
.
filter
(
n
=>
!
n
.
individual_note
);
return
discussions
.
length
;
return
discussions
.
length
;
...
@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
...
@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return
state
.
notes
.
filter
(
n
=>
!
n
.
individual_note
&&
!
resolvedMap
[
n
.
id
]);
return
state
.
notes
.
filter
(
n
=>
!
n
.
individual_note
&&
!
resolvedMap
[
n
.
id
]);
};
};
export
const
resolvedDiscussionsById
=
(
state
)
=>
{
export
const
resolvedDiscussionsById
=
state
=>
{
const
map
=
{};
const
map
=
{};
state
.
notes
.
forEach
(
(
n
)
=>
{
state
.
notes
.
forEach
(
n
=>
{
if
(
n
.
notes
)
{
if
(
n
.
notes
)
{
const
resolved
=
n
.
notes
.
every
(
note
=>
note
.
resolved
&&
!
note
.
system
);
const
resolved
=
n
.
notes
.
every
(
note
=>
note
.
resolved
&&
!
note
.
system
);
...
...
app/assets/javascripts/notes/stores/mutations.js
View file @
fdc9ae2e
...
@@ -7,7 +7,7 @@ export default {
...
@@ -7,7 +7,7 @@ export default {
[
types
.
ADD_NEW_NOTE
](
state
,
note
)
{
[
types
.
ADD_NEW_NOTE
](
state
,
note
)
{
const
{
discussion_id
,
type
}
=
note
;
const
{
discussion_id
,
type
}
=
note
;
const
[
exists
]
=
state
.
notes
.
filter
(
n
=>
n
.
id
===
note
.
discussion_id
);
const
[
exists
]
=
state
.
notes
.
filter
(
n
=>
n
.
id
===
note
.
discussion_id
);
const
isDiscussion
=
(
type
===
constants
.
DISCUSSION_NOTE
)
;
const
isDiscussion
=
type
===
constants
.
DISCUSSION_NOTE
;
if
(
!
exists
)
{
if
(
!
exists
)
{
const
noteData
=
{
const
noteData
=
{
...
@@ -63,13 +63,15 @@ export default {
...
@@ -63,13 +63,15 @@ export default {
const
note
=
notes
[
i
];
const
note
=
notes
[
i
];
const
children
=
note
.
notes
;
const
children
=
note
.
notes
;
if
(
children
.
length
&&
!
note
.
individual_note
)
{
// remove placeholder from discussions
if
(
children
.
length
&&
!
note
.
individual_note
)
{
// remove placeholder from discussions
for
(
let
j
=
children
.
length
-
1
;
j
>=
0
;
j
-=
1
)
{
for
(
let
j
=
children
.
length
-
1
;
j
>=
0
;
j
-=
1
)
{
if
(
children
[
j
].
isPlaceholderNote
)
{
if
(
children
[
j
].
isPlaceholderNote
)
{
children
.
splice
(
j
,
1
);
children
.
splice
(
j
,
1
);
}
}
}
}
}
else
if
(
note
.
isPlaceholderNote
)
{
// remove placeholders from state root
}
else
if
(
note
.
isPlaceholderNote
)
{
// remove placeholders from state root
notes
.
splice
(
i
,
1
);
notes
.
splice
(
i
,
1
);
}
}
}
}
...
@@ -89,10 +91,10 @@ export default {
...
@@ -89,10 +91,10 @@ export default {
[
types
.
SET_INITIAL_NOTES
](
state
,
notesData
)
{
[
types
.
SET_INITIAL_NOTES
](
state
,
notesData
)
{
const
notes
=
[];
const
notes
=
[];
notesData
.
forEach
(
(
note
)
=>
{
notesData
.
forEach
(
note
=>
{
// To support legacy notes, should be very rare case.
// To support legacy notes, should be very rare case.
if
(
note
.
individual_note
&&
note
.
notes
.
length
>
1
)
{
if
(
note
.
individual_note
&&
note
.
notes
.
length
>
1
)
{
note
.
notes
.
forEach
(
(
n
)
=>
{
note
.
notes
.
forEach
(
n
=>
{
notes
.
push
({
notes
.
push
({
...
note
,
...
note
,
notes
:
[
n
],
// override notes array to only have one item to mimick individual_note
notes
:
[
n
],
// override notes array to only have one item to mimick individual_note
...
@@ -103,7 +105,7 @@ export default {
...
@@ -103,7 +105,7 @@ export default {
notes
.
push
({
notes
.
push
({
...
note
,
...
note
,
expanded
:
(
oldNote
?
oldNote
.
expanded
:
note
.
expanded
)
,
expanded
:
oldNote
?
oldNote
.
expanded
:
note
.
expanded
,
});
});
}
}
});
});
...
@@ -128,7 +130,9 @@ export default {
...
@@ -128,7 +130,9 @@ export default {
notesArr
.
push
({
notesArr
.
push
({
individual_note
:
true
,
individual_note
:
true
,
isPlaceholderNote
:
true
,
isPlaceholderNote
:
true
,
placeholderType
:
data
.
isSystemNote
?
constants
.
SYSTEM_NOTE
:
constants
.
NOTE
,
placeholderType
:
data
.
isSystemNote
?
constants
.
SYSTEM_NOTE
:
constants
.
NOTE
,
notes
:
[
notes
:
[
{
{
body
:
data
.
noteBody
,
body
:
data
.
noteBody
,
...
@@ -141,12 +145,16 @@ export default {
...
@@ -141,12 +145,16 @@ export default {
const
{
awardName
,
note
}
=
data
;
const
{
awardName
,
note
}
=
data
;
const
{
id
,
name
,
username
}
=
state
.
userData
;
const
{
id
,
name
,
username
}
=
state
.
userData
;
const
hasEmojiAwardedByCurrentUser
=
note
.
award_emoji
const
hasEmojiAwardedByCurrentUser
=
note
.
award_emoji
.
filter
(
.
filter
(
emoji
=>
emoji
.
name
===
data
.
awardName
&&
emoji
.
user
.
id
===
id
);
emoji
=>
emoji
.
name
===
data
.
awardName
&&
emoji
.
user
.
id
===
id
,
);
if
(
hasEmojiAwardedByCurrentUser
.
length
)
{
if
(
hasEmojiAwardedByCurrentUser
.
length
)
{
// If current user has awarded this emoji, remove it.
// If current user has awarded this emoji, remove it.
note
.
award_emoji
.
splice
(
note
.
award_emoji
.
indexOf
(
hasEmojiAwardedByCurrentUser
[
0
]),
1
);
note
.
award_emoji
.
splice
(
note
.
award_emoji
.
indexOf
(
hasEmojiAwardedByCurrentUser
[
0
]),
1
,
);
}
else
{
}
else
{
note
.
award_emoji
.
push
({
note
.
award_emoji
.
push
({
name
:
awardName
,
name
:
awardName
,
...
...
app/assets/javascripts/notes/stores/utils.js
View file @
fdc9ae2e
...
@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
...
@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const
REGEX_QUICK_ACTIONS
=
/^
\/\w
+.*$/gm
;
const
REGEX_QUICK_ACTIONS
=
/^
\/\w
+.*$/gm
;
export
const
findNoteObjectById
=
(
notes
,
id
)
=>
notes
.
filter
(
n
=>
n
.
id
===
id
)[
0
];
export
const
findNoteObjectById
=
(
notes
,
id
)
=>
notes
.
filter
(
n
=>
n
.
id
===
id
)[
0
];
export
const
getQuickActionText
=
(
note
)
=>
{
export
const
getQuickActionText
=
note
=>
{
let
text
=
'
Applying command
'
;
let
text
=
'
Applying command
'
;
const
quickActions
=
AjaxCache
.
get
(
gl
.
GfmAutoComplete
.
dataSources
.
commands
)
||
[];
const
quickActions
=
AjaxCache
.
get
(
gl
.
GfmAutoComplete
.
dataSources
.
commands
)
||
[];
const
executedCommands
=
quickActions
.
filter
(
(
command
)
=>
{
const
executedCommands
=
quickActions
.
filter
(
command
=>
{
const
commandRegex
=
new
RegExp
(
`/
${
command
.
name
}
`
);
const
commandRegex
=
new
RegExp
(
`/
${
command
.
name
}
`
);
return
commandRegex
.
test
(
note
);
return
commandRegex
.
test
(
note
);
});
});
...
@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => {
...
@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => {
export
const
hasQuickActions
=
note
=>
REGEX_QUICK_ACTIONS
.
test
(
note
);
export
const
hasQuickActions
=
note
=>
REGEX_QUICK_ACTIONS
.
test
(
note
);
export
const
stripQuickActions
=
note
=>
note
.
replace
(
REGEX_QUICK_ACTIONS
,
''
).
trim
();
export
const
stripQuickActions
=
note
=>
note
.
replace
(
REGEX_QUICK_ACTIONS
,
''
).
trim
();
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