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
Jérome Perrin
gitlab-ce
Commits
88bd9bac
Commit
88bd9bac
authored
Mar 16, 2018
by
Mike Greiling
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'acet-notes-prettified' into 'master'
Prettify notes app See merge request gitlab-org/gitlab-ce!17812
parents
bcc04515
fdc9ae2e
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 @
88bd9bac
...
...
@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import
store
from
'
../notes/stores
'
;
export
default
function
initMrNotes
()
{
new
Vue
({
// eslint-disable-line
// eslint-disable-next-line no-new
new
Vue
({
el
:
'
#js-vue-mr-discussions
'
,
components
:
{
notesApp
,
},
data
()
{
const
notesDataset
=
document
.
getElementById
(
'
js-vue-mr-discussions
'
).
dataset
;
const
notesDataset
=
document
.
getElementById
(
'
js-vue-mr-discussions
'
)
.
dataset
;
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
:
JSON
.
parse
(
notesDataset
.
currentUserData
),
...
...
@@ -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
'
,
components
:
{
discussionCounter
,
...
...
app/assets/javascripts/notes.js
View file @
88bd9bac
...
...
@@ -28,7 +28,13 @@ import GLForm from './gl_form';
import
loadAwardsHandler
from
'
./awards_handler
'
;
import
Autosave
from
'
./autosave
'
;
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
{
localTimeAgo
}
from
'
./lib/utils/datetime_utility
'
;
...
...
@@ -42,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const
REGEX_QUICK_ACTIONS
=
/^
\/\w
+.*$/gm
;
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
)
{
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 {
this
.
updatedNotesTrackingMap
=
{};
this
.
last_fetched_at
=
last_fetched_at
;
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
.
maxPollingSteps
=
4
;
...
...
@@ -93,15 +112,17 @@ export default class Notes {
this
.
taskList
=
new
TaskList
({
dataType
:
'
note
'
,
fieldName
:
'
note
'
,
selector
:
'
.notes
'
selector
:
'
.notes
'
,
});
this
.
collapseLongCommitList
();
this
.
setViewType
(
view
);
// We are in the Merge Requests page so we need another edit form for Changes tab
if
(
getPagePath
(
1
)
===
'
merge_requests
'
)
{
$
(
'
.note-edit-form
'
).
clone
()
.
addClass
(
'
mr-note-edit-form
'
).
insertAfter
(
'
.note-edit-form
'
);
$
(
'
.note-edit-form
'
)
.
clone
()
.
addClass
(
'
mr-note-edit-form
'
)
.
insertAfter
(
'
.note-edit-form
'
);
}
const
hash
=
getLocationHash
();
...
...
@@ -117,7 +138,9 @@ export default class Notes {
}
addBinding
()
{
this
.
$wrapperEl
=
hasVueMRDiscussionsCookie
()
?
$
(
document
).
find
(
'
.diffs
'
)
:
$
(
document
);
this
.
$wrapperEl
=
hasVueMRDiscussionsCookie
()
?
$
(
document
).
find
(
'
.diffs
'
)
:
$
(
document
);
// Edit note link
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-edit
'
,
this
.
showEditForm
.
bind
(
this
));
...
...
@@ -125,27 +148,55 @@ export default class Notes {
// 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-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
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-comment-resolve-button
'
,
this
.
postComment
);
// remove a note (in general)
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-delete
'
,
this
.
removeNote
);
// 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
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-note-discard
'
,
this
.
resetMainTargetForm
);
// 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
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-discussion-reply-button
'
,
this
.
onReplyToDiscussionNote
);
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-discussion-reply-button
'
,
this
.
onReplyToDiscussionNote
,
);
// add diff note
this
.
$wrapperEl
.
on
(
'
click
'
,
'
.js-add-diff-note-button
'
,
this
.
onAddDiffNote
);
// 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
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
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
);
// fetch notes when tab becomes visible
...
...
@@ -154,9 +205,21 @@ export default class Notes {
this
.
$wrapperEl
.
on
(
'
issuable:change
'
,
this
.
refresh
);
// 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-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
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-discussion-note-form
'
,
this
.
addDiscussionNote
,
);
this
.
$wrapperEl
.
on
(
'
ajax:success
'
,
'
.js-main-target-form
'
,
this
.
resetMainTargetForm
,
);
this
.
$wrapperEl
.
on
(
'
ajax:complete
'
,
'
.js-main-target-form
'
,
this
.
reenableTargetFormSubmitButton
,
);
// when a key is clicked on the notes
this
.
$wrapperEl
.
on
(
'
keydown
'
,
'
.js-note-text
'
,
this
.
keydownNoteText
);
// When the URL fragment/hash has changed, `#note_xxx`
...
...
@@ -195,10 +258,16 @@ export default class Notes {
}
static
initCommentTypeToggle
(
form
)
{
const
dropdownTrigger
=
form
.
querySelector
(
'
.js-comment-type-dropdown .dropdown-toggle
'
);
const
dropdownList
=
form
.
querySelector
(
'
.js-comment-type-dropdown .dropdown-menu
'
);
const
dropdownTrigger
=
form
.
querySelector
(
'
.js-comment-type-dropdown .dropdown-toggle
'
,
);
const
dropdownList
=
form
.
querySelector
(
'
.js-comment-type-dropdown .dropdown-menu
'
,
);
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
reopenButton
=
form
.
querySelector
(
'
.js-note-target-reopen
'
);
...
...
@@ -215,7 +284,13 @@ export default class Notes {
}
keydownNoteText
(
e
)
{
var
$textarea
,
discussionNoteForm
,
editNote
,
myLastNote
,
myLastNoteEditBtn
,
newText
,
originalText
;
var
$textarea
,
discussionNoteForm
,
editNote
,
myLastNote
,
myLastNoteEditBtn
,
newText
,
originalText
;
if
(
isMetaKey
(
e
))
{
return
;
}
...
...
@@ -227,7 +302,12 @@ export default class Notes {
if
(
$textarea
.
val
()
!==
''
)
{
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
)
{
myLastNoteEditBtn
=
myLastNote
.
find
(
'
.js-note-edit
'
);
return
myLastNoteEditBtn
.
trigger
(
'
click
'
,
[
true
,
myLastNote
]);
...
...
@@ -238,7 +318,9 @@ export default class Notes {
discussionNoteForm
=
$textarea
.
closest
(
'
.js-discussion-note-form
'
);
if
(
discussionNoteForm
.
length
)
{
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
;
}
}
...
...
@@ -250,7 +332,9 @@ export default class Notes {
originalText
=
$textarea
.
closest
(
'
form
'
).
data
(
'
originalNote
'
);
newText
=
$textarea
.
val
();
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
;
}
}
...
...
@@ -263,11 +347,14 @@ export default class Notes {
if
(
Notes
.
interval
)
{
clearInterval
(
Notes
.
interval
);
}
return
Notes
.
interval
=
setInterval
((
function
(
_this
)
{
return
function
()
{
return
_this
.
refresh
();
};
})(
this
),
this
.
pollingInterval
);
return
(
Notes
.
interval
=
setInterval
(
(
function
(
_this
)
{
return
function
()
{
return
_this
.
refresh
();
};
})(
this
),
this
.
pollingInterval
,
));
}
refresh
()
{
...
...
@@ -283,20 +370,23 @@ export default class Notes {
this
.
refreshing
=
true
;
axios
.
get
(
`
${
this
.
notes_url
}
?html=true`
,
{
headers
:
{
'
X-Last-Fetched-At
'
:
this
.
last_fetched_at
,
},
}).
then
(({
data
})
=>
{
const
notes
=
data
.
notes
;
this
.
last_fetched_at
=
data
.
last_fetched_at
;
this
.
setPollingInterval
(
data
.
notes
.
length
);
$
.
each
(
notes
,
(
i
,
note
)
=>
this
.
renderNote
(
note
));
this
.
refreshing
=
false
;
}).
catch
(()
=>
{
this
.
refreshing
=
false
;
});
axios
.
get
(
`
${
this
.
notes_url
}
?html=true`
,
{
headers
:
{
'
X-Last-Fetched-At
'
:
this
.
last_fetched_at
,
},
})
.
then
(({
data
})
=>
{
const
notes
=
data
.
notes
;
this
.
last_fetched_at
=
data
.
last_fetched_at
;
this
.
setPollingInterval
(
data
.
notes
.
length
);
$
.
each
(
notes
,
(
i
,
note
)
=>
this
.
renderNote
(
note
));
this
.
refreshing
=
false
;
})
.
catch
(()
=>
{
this
.
refreshing
=
false
;
});
}
/**
...
...
@@ -312,7 +402,8 @@ export default class Notes {
if
(
shouldReset
==
null
)
{
shouldReset
=
true
;
}
nthInterval
=
this
.
basePollingInterval
*
Math
.
pow
(
2
,
this
.
maxPollingSteps
-
1
);
nthInterval
=
this
.
basePollingInterval
*
Math
.
pow
(
2
,
this
.
maxPollingSteps
-
1
);
if
(
shouldReset
)
{
this
.
pollingInterval
=
this
.
basePollingInterval
;
}
else
if
(
this
.
pollingInterval
<
nthInterval
)
{
...
...
@@ -331,12 +422,17 @@ export default class Notes {
if
(
'
emoji_award
'
in
noteEntity
.
commands_changes
)
{
votesBlock
=
$
(
'
.js-awards-block
'
).
eq
(
0
);
loadAwardsHandler
().
then
((
awardsHandler
)
=>
{
awardsHandler
.
addAwardToEmojiBar
(
votesBlock
,
noteEntity
.
commands_changes
.
emoji_award
);
awardsHandler
.
scrollToAwards
();
}).
catch
(()
=>
{
// ignore
});
loadAwardsHandler
()
.
then
(
awardsHandler
=>
{
awardsHandler
.
addAwardToEmojiBar
(
votesBlock
,
noteEntity
.
commands_changes
.
emoji_award
,
);
awardsHandler
.
scrollToAwards
();
})
.
catch
(()
=>
{
// ignore
});
}
}
}
...
...
@@ -381,11 +477,17 @@ export default class Notes {
if
(
!
noteEntity
.
valid
)
{
if
(
noteEntity
.
errors
&&
noteEntity
.
errors
.
commands_only
)
{
if
(
noteEntity
.
commands_changes
&&
Object
.
keys
(
noteEntity
.
commands_changes
).
length
>
0
)
{
if
(
noteEntity
.
commands_changes
&&
Object
.
keys
(
noteEntity
.
commands_changes
).
length
>
0
)
{
$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
();
}
return
;
...
...
@@ -407,28 +509,30 @@ export default class Notes {
this
.
setupNewNote
(
$newNote
);
this
.
refresh
();
return
this
.
updateNotesCount
(
1
);
}
// 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
))
{
}
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.
const
isEditing
=
$note
.
hasClass
(
'
is-editing
'
);
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
currentContent
=
$textarea
.
val
();
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const
sanitizedNoteNote
=
normalizeNewlines
(
noteEntity
.
note
);
const
isTextareaUntouched
=
currentContent
===
initialContent
||
currentContent
===
sanitizedNoteNote
;
const
isTextareaUntouched
=
currentContent
===
initialContent
||
currentContent
===
sanitizedNoteNote
;
if
(
isEditing
&&
isTextareaUntouched
)
{
$textarea
.
val
(
noteEntity
.
note
);
this
.
updatedNotesTrackingMap
[
noteEntity
.
id
]
=
noteEntity
;
}
else
if
(
isEditing
&&
!
isTextareaUntouched
)
{
}
else
if
(
isEditing
&&
!
isTextareaUntouched
)
{
this
.
putConflictEditWarningInPlace
(
noteEntity
,
$note
);
this
.
updatedNotesTrackingMap
[
noteEntity
.
id
]
=
noteEntity
;
}
else
{
}
else
{
const
$updatedNote
=
Notes
.
animateUpdateNote
(
noteEntity
.
html
,
$note
);
this
.
setupNewNote
(
$updatedNote
);
}
...
...
@@ -452,17 +556,31 @@ export default class Notes {
}
this
.
note_ids
.
push
(
noteEntity
.
id
);
form
=
$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
}
`
);
form
=
$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
)
{
row
=
form
;
}
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?
discussionContainer
=
$
(
`.notes[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
);
discussionContainer
=
$
(
`.notes[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
,
);
if
(
!
discussionContainer
.
length
)
{
discussionContainer
=
form
.
closest
(
'
.discussion
'
).
find
(
'
.notes
'
);
}
...
...
@@ -470,25 +588,42 @@ export default class Notes {
if
(
noteEntity
.
diff_discussion_html
)
{
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
row
.
after
(
$discussion
);
}
else
{
// Merge new discussion HTML in
var
$notes
=
$discussion
.
find
(
`.notes[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
);
var
contentContainerClass
=
'
.
'
+
$notes
.
closest
(
'
.notes_content
'
)
.
attr
(
'
class
'
)
.
split
(
'
'
)
.
join
(
'
.
'
);
row
.
find
(
contentContainerClass
+
'
.content
'
).
append
(
$notes
.
closest
(
'
.content
'
).
children
());
var
$notes
=
$discussion
.
find
(
`.notes[data-discussion-id="
${
noteEntity
.
discussion_id
}
"]`
,
);
var
contentContainerClass
=
'
.
'
+
$notes
.
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
const
page
=
$
(
'
body
'
).
attr
(
'
data-page
'
);
if
((
page
&&
page
.
indexOf
(
'
projects:merge_request
'
)
!==
-
1
)
||
!
noteEntity
.
diff_discussion_html
)
{
if
(
(
page
&&
page
.
indexOf
(
'
projects:merge_request
'
)
!==
-
1
)
||
!
noteEntity
.
diff_discussion_html
)
{
if
(
!
hasVueMRDiscussionsCookie
())
{
Notes
.
animateAppendNote
(
noteEntity
.
discussion_html
,
$
(
'
.main-notes-list
'
));
Notes
.
animateAppendNote
(
noteEntity
.
discussion_html
,
$
(
'
.main-notes-list
'
),
);
}
}
}
else
{
...
...
@@ -496,7 +631,10 @@ export default class Notes {
Notes
.
animateAppendNote
(
noteEntity
.
html
,
discussionContainer
);
}
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
&&
noteEntity
.
discussion_resolvable
)
{
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
&&
noteEntity
.
discussion_resolvable
)
{
gl
.
diffNotesCompileComponents
();
this
.
renderDiscussionAvatar
(
diffAvatarContainer
,
noteEntity
);
...
...
@@ -508,7 +646,8 @@ export default class Notes {
}
getLineHolder
(
changesDiscussionContainer
)
{
return
$
(
changesDiscussionContainer
).
closest
(
'
.notes_holder
'
)
return
$
(
changesDiscussionContainer
)
.
closest
(
'
.notes_holder
'
)
.
prevAll
(
'
.line_holder
'
)
.
first
()
.
get
(
0
);
...
...
@@ -541,8 +680,14 @@ export default class Notes {
form
.
find
(
'
.js-errors
'
).
remove
();
// reset text and preview
form
.
find
(
'
.js-md-write-button
'
).
click
();
form
.
find
(
'
.js-note-text
'
).
val
(
''
).
trigger
(
'
input
'
);
form
.
find
(
'
.js-note-text
'
).
data
(
'
autosave
'
).
reset
();
form
.
find
(
'
.js-note-text
'
)
.
val
(
''
)
.
trigger
(
'
input
'
);
form
.
find
(
'
.js-note-text
'
)
.
data
(
'
autosave
'
)
.
reset
();
var
event
=
document
.
createEvent
(
'
Event
'
);
event
.
initEvent
(
'
autosize:update
'
,
true
,
false
);
...
...
@@ -578,7 +723,10 @@ export default class Notes {
form
.
find
(
'
#note_type
'
).
val
(
''
);
form
.
find
(
'
#note_project_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
'
);
if
(
form
.
length
)
{
...
...
@@ -632,11 +780,17 @@ export default class Notes {
}
else
if
(
$form
.
hasClass
(
'
js-discussion-note-form
'
))
{
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
)
{
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 {
}
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
isAllowed
=
true
;
if
(
currentContent
===
initialContent
)
{
this
.
removeNoteEditForm
(
$el
);
}
else
{
}
else
{
var
$buttons
=
$el
.
find
(
'
.note-form-actions
'
);
var
isWidgetVisible
=
isInViewport
(
$el
.
get
(
0
));
...
...
@@ -754,8 +910,7 @@ export default class Notes {
this
.
setupNewNote
(
$newNote
);
// Now that we have taken care of the update, clear it out
delete
this
.
updatedNotesTrackingMap
[
noteId
];
}
else
{
}
else
{
$note
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
this
.
removeNoteEditForm
(
$note
);
}
...
...
@@ -788,7 +943,9 @@ export default class Notes {
form
.
removeClass
(
'
current-note-edit-form
'
);
form
.
find
(
'
.js-finish-edit-warning
'
).
hide
();
// 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 {
$note
=
$
(
e
.
currentTarget
).
closest
(
'
.note
'
);
noteElId
=
$note
.
attr
(
'
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
'
)
.
prev
(
'
.line_holder
'
);
$
(
`.note[id="
${
noteElId
}
"]`
).
each
((
function
(
_this
)
{
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
// where $('#noteId') would return only one.
return
function
(
i
,
el
)
{
var
$note
,
$notes
;
$note
=
$
(
el
);
$notes
=
$note
.
closest
(
'
.discussion-notes
'
);
const
discussionId
=
$
(
'
.notes
'
,
$notes
).
data
(
'
discussionId
'
);
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
)
{
if
(
gl
.
diffNoteApps
[
noteElId
])
{
gl
.
diffNoteApps
[
noteElId
].
$destroy
();
$
(
`.note[id="
${
noteElId
}
"]`
).
each
(
(
function
(
_this
)
{
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
// where $('#noteId') would return only one.
return
function
(
i
,
el
)
{
var
$note
,
$notes
;
$note
=
$
(
el
);
$notes
=
$note
.
closest
(
'
.discussion-notes
'
);
const
discussionId
=
$
(
'
.notes
'
,
$notes
).
data
(
'
discussionId
'
);
if
(
typeof
gl
.
diffNotesCompileComponents
!==
'
undefined
'
)
{
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
.
checkMergeRequestStatus
();
...
...
@@ -935,7 +1101,12 @@ export default class Notes {
// DiffNote
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-new-discussion
'
).
remove
();
this
.
setupNoteForm
(
form
);
...
...
@@ -971,7 +1142,7 @@ export default class Notes {
this
.
toggleDiffNote
({
target
:
$link
,
lineType
:
link
.
dataset
.
lineType
,
showReplyInput
showReplyInput
,
});
}
...
...
@@ -987,7 +1158,9 @@ export default class Notes {
// Setup comment form
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
'
);
if
(
$form
.
length
===
0
)
{
...
...
@@ -1000,13 +1173,17 @@ export default class Notes {
this
.
setupDiscussionNoteForm
(
$link
,
newForm
);
}
toggleDiffNote
({
target
,
lineType
,
forceShow
,
showReplyInput
=
false
,
})
{
var
$link
,
addForm
,
hasNotes
,
newForm
,
noteForm
,
replyButton
,
row
,
rowCssToAdd
,
targetContent
,
isDiffCommentAvatar
;
toggleDiffNote
({
target
,
lineType
,
forceShow
,
showReplyInput
=
false
})
{
var
$link
,
addForm
,
hasNotes
,
newForm
,
noteForm
,
replyButton
,
row
,
rowCssToAdd
,
targetContent
,
isDiffCommentAvatar
;
$link
=
$
(
target
);
row
=
$link
.
closest
(
'
tr
'
);
const
nextRow
=
row
.
next
();
...
...
@@ -1018,11 +1195,13 @@ export default class Notes {
hasNotes
=
nextRow
.
is
(
'
.notes_holder
'
);
addForm
=
false
;
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
if
(
this
.
isParallelView
())
{
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`
;
let
notesContent
=
targetRow
.
find
(
notesContentSelector
);
...
...
@@ -1050,7 +1229,9 @@ export default class Notes {
notesContent
=
targetRow
.
find
(
notesContentSelector
);
addForm
=
true
;
}
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
showNow
=
forceShow
===
true
||
(
!
isCurrentlyShown
&&
!
isForced
);
...
...
@@ -1077,11 +1258,12 @@ export default class Notes {
row
=
form
.
closest
(
'
tr
'
);
glForm
=
form
.
data
(
'
glForm
'
);
glForm
.
destroy
();
form
.
find
(
'
.js-note-text
'
).
data
(
'
autosave
'
).
reset
();
// show the reply button (will only work for replies)
form
.
prev
(
'
.discussion-reply-holder
'
)
.
show
();
.
find
(
'
.js-note-text
'
)
.
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
'
))
{
// remove temporary row for diff lines
return
row
.
remove
();
...
...
@@ -1122,7 +1304,9 @@ export default class Notes {
var
filename
,
form
;
form
=
$
(
this
).
closest
(
'
form
'
);
// get only the basename
filename
=
$
(
this
).
val
().
replace
(
/^.*
[\\\/]
/
,
''
);
filename
=
$
(
this
)
.
val
()
.
replace
(
/^.*
[\\\/]
/
,
''
);
return
form
.
find
(
'
.js-attachment-filename
'
).
text
(
filename
);
}
...
...
@@ -1194,12 +1378,16 @@ export default class Notes {
this
.
glForm
=
new
GLForm
(
$editForm
.
find
(
'
form
'
),
this
.
enableGFM
);
$editForm
.
find
(
'
form
'
)
$editForm
.
find
(
'
form
'
)
.
attr
(
'
action
'
,
`
${
postUrl
}
?html=true`
)
.
attr
(
'
data-remote
'
,
'
true
'
);
$editForm
.
find
(
'
.js-form-target-id
'
).
val
(
targetId
);
$editForm
.
find
(
'
.js-form-target-type
'
).
val
(
targetType
);
$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
(
'
.referenced-users
'
).
hide
();
}
...
...
@@ -1208,7 +1396,9 @@ export default class Notes {
if
(
$note
.
find
(
'
.js-conflict-edit-warning
'
).
length
===
0
)
{
const
$alert
=
$
(
`<div class="js-conflict-edit-warning alert alert-danger">
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
</a>
to ensure information is not lost
...
...
@@ -1218,12 +1408,15 @@ export default class Notes {
}
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
)
{
const
el
=
$container
.
find
(
'
.js-code-placeholder
'
).
get
(
0
);
new
Vue
({
// eslint-disable-line no-new
new
Vue
({
// eslint-disable-line no-new
el
,
components
:
{
SkeletonLoadingContainer
,
...
...
@@ -1248,7 +1441,9 @@ export default class Notes {
$container
.
find
(
'
.line_content
'
).
html
(
$
(
`
<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>
`
),
);
...
...
@@ -1266,7 +1461,8 @@ export default class Notes {
const
fileHolder
=
$container
.
find
(
'
.file-holder
'
);
const
url
=
fileHolder
.
data
(
'
linesPath
'
);
axios
.
get
(
url
)
axios
.
get
(
url
)
.
then
(({
data
})
=>
{
Notes
.
renderDiffContent
(
$container
,
data
);
})
...
...
@@ -1277,9 +1473,14 @@ export default class Notes {
toggleCommitList
(
e
)
{
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
'
);
}
...
...
@@ -1289,11 +1490,17 @@ export default class Notes {
* intrusive.
*/
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
)
{
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
);
...
...
@@ -1301,7 +1508,9 @@ export default class Notes {
$systemNote
.
find
(
'
.note-text
'
).
addClass
(
'
system-note-commit-list
'
);
$systemNote
.
find
(
'
.system-note-commit-list-toggler
'
).
show
();
}
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 {
cleanForm
(
$form
)
{
// Remove JS classes that are not needed here
$form
.
find
(
'
.js-comment-type-dropdown
'
)
.
removeClass
(
'
btn-group
'
);
$form
.
find
(
'
.js-comment-type-dropdown
'
).
removeClass
(
'
btn-group
'
);
// Remove dropdown
$form
.
find
(
'
.dropdown-menu
'
)
.
remove
();
$form
.
find
(
'
.dropdown-menu
'
).
remove
();
return
$form
;
}
...
...
@@ -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
const
sanitizedNoteEntityText
=
normalizeNewlines
(
noteEntity
.
note
.
trim
());
const
currentNoteText
=
normalizeNewlines
(
$note
.
find
(
'
.original-note-content
'
).
first
().
text
().
trim
()
$note
.
find
(
'
.original-note-content
'
)
.
first
()
.
text
()
.
trim
(),
);
return
sanitizedNoteEntityText
!==
currentNoteText
;
}
...
...
@@ -1435,7 +1644,14 @@ export default class Notes {
* Once comment is _actually_ posted on server, we will have final 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
$tempNote
=
$
(
`<li id="
${
uniqueId
}
" class="note being-posted fade-in-half timeline-entry">
...
...
@@ -1449,8 +1665,12 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/
${
_
.
escape
(
currentUsername
)}
">
<span class="hidden-xs">
${
_
.
escape
(
currentUsername
)}
</span>
<span class="note-headline-light">
${
_
.
escape
(
currentUsername
)}
</span>
<span class="hidden-xs">
${
_
.
escape
(
currentUsername
,
)}
</span>
<span class="note-headline-light">
${
_
.
escape
(
currentUsername
,
)}
</span>
</a>
</div>
</div>
...
...
@@ -1461,11 +1681,13 @@ export default class Notes {
</div>
</div>
</div>
</li>`
</li>`
,
);
$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
;
}
...
...
@@ -1481,7 +1703,7 @@ export default class Notes {
<i>
${
formContent
}
</i>
</div>
</div>
</li>`
</li>`
,
);
return
$tempNote
;
...
...
@@ -1513,11 +1735,22 @@ export default class Notes {
const
$submitBtn
=
$
(
e
.
target
);
let
$form
=
$submitBtn
.
parents
(
'
form
'
);
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
isDiscussionForm
=
$form
.
hasClass
(
'
js-discussion-note-form
'
);
const
isDiscussionResolve
=
$submitBtn
.
hasClass
(
'
js-comment-resolve-button
'
);
const
{
formData
,
formContent
,
formAction
,
formContentOriginal
}
=
this
.
getFormData
(
$form
);
const
isDiscussionResolve
=
$submitBtn
.
hasClass
(
'
js-comment-resolve-button
'
,
);
const
{
formData
,
formContent
,
formAction
,
formContentOriginal
,
}
=
this
.
getFormData
(
$form
);
let
noteUniqueId
;
let
systemNoteUniqueId
;
let
hasQuickActions
=
false
;
...
...
@@ -1547,23 +1780,30 @@ export default class Notes {
// Show placeholder note
if
(
tempFormContent
)
{
noteUniqueId
=
_
.
uniqueId
(
'
tempNote_
'
);
$notesContainer
.
append
(
this
.
createPlaceholderNote
({
formContent
:
tempFormContent
,
uniqueId
:
noteUniqueId
,
isDiscussionNote
,
currentUsername
:
gon
.
current_username
,
currentUserFullname
:
gon
.
current_user_fullname
,
currentUserAvatar
:
gon
.
current_user_avatar_url
,
}));
$notesContainer
.
append
(
this
.
createPlaceholderNote
({
formContent
:
tempFormContent
,
uniqueId
:
noteUniqueId
,
isDiscussionNote
,
currentUsername
:
gon
.
current_username
,
currentUserFullname
:
gon
.
current_user_fullname
,
currentUserAvatar
:
gon
.
current_user_avatar_url
,
}),
);
}
// Show placeholder system note
if
(
hasQuickActions
)
{
systemNoteUniqueId
=
_
.
uniqueId
(
'
tempSystemNote_
'
);
$notesContainer
.
append
(
this
.
createPlaceholderSystemNote
({
formContent
:
this
.
getQuickActionDescription
(
formContent
,
AjaxCache
.
get
(
gl
.
GfmAutoComplete
.
dataSources
.
commands
)),
uniqueId
:
systemNoteUniqueId
,
}));
$notesContainer
.
append
(
this
.
createPlaceholderSystemNote
({
formContent
:
this
.
getQuickActionDescription
(
formContent
,
AjaxCache
.
get
(
gl
.
GfmAutoComplete
.
dataSources
.
commands
),
),
uniqueId
:
systemNoteUniqueId
,
}),
);
}
// Clear the form textarea
...
...
@@ -1577,8 +1817,9 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
((
res
)
=>
{
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
(
res
=>
{
const
note
=
res
.
data
;
// Submission successful! remove placeholder
...
...
@@ -1595,7 +1836,9 @@ export default class Notes {
// Reset cached commands list when command is applied
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
...
...
@@ -1640,11 +1883,14 @@ export default class Notes {
// append flash-container to the Notes list
if
(
$notesContainer
.
length
)
{
$notesContainer
.
append
(
'
<div class="flash-container" style="display: none;"></div>
'
);
$notesContainer
.
append
(
'
<div class="flash-container" style="display: none;"></div>
'
,
);
}
Notes
.
refreshVueNotes
();
}
else
if
(
isMainForm
)
{
// Check if this was main thread comment
}
else
if
(
isMainForm
)
{
// Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
this
.
addNote
(
$form
,
note
);
this
.
reenableTargetFormSubmitButton
(
e
);
...
...
@@ -1655,7 +1901,8 @@ export default class Notes {
}
$form
.
trigger
(
'
ajax:success
'
,
[
note
]);
}).
catch
(()
=>
{
})
.
catch
(()
=>
{
// Submission failed, remove placeholder note and show Flash error message
$notesContainer
.
find
(
`#
${
noteUniqueId
}
`
).
remove
();
...
...
@@ -1675,7 +1922,9 @@ export default class Notes {
// Show form again on UI on failure
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
]);
$form
=
$notesContainer
.
parent
().
find
(
'
form
'
);
}
...
...
@@ -1720,12 +1969,19 @@ export default class Notes {
// Show updated comment content temporarily
$noteBodyText
.
html
(
formContent
);
$editingNote
.
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>
'
);
$editingNote
.
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 */
// Make request to update comment on server
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
axios
.
post
(
`
${
formAction
}
?html=true`
,
formData
)
.
then
(({
data
})
=>
{
// Submission successful! render final note element
this
.
updateNote
(
data
,
$editingNote
);
...
...
app/assets/javascripts/notes/components/comment_form.vue
View file @
88bd9bac
<
script
>
import
$
from
'
jquery
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
_
from
'
underscore
'
;
import
Autosize
from
'
autosize
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
Flash
from
'
../../flash
'
;
import
Autosave
from
'
../../autosave
'
;
import
TaskList
from
'
../../task_list
'
;
import
{
capitalizeFirstCharacter
,
convertToCamelCase
}
from
'
../../lib/utils/text_utility
'
;
import
*
as
constants
from
'
../constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
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
'
;
import
$
from
'
jquery
'
;
import
{
mapActions
,
mapGetters
,
mapState
}
from
'
vuex
'
;
import
_
from
'
underscore
'
;
import
Autosize
from
'
autosize
'
;
import
{
__
,
sprintf
}
from
'
~/locale
'
;
import
Flash
from
'
../../flash
'
;
import
Autosave
from
'
../../autosave
'
;
import
TaskList
from
'
../../task_list
'
;
import
{
capitalizeFirstCharacter
,
convertToCamelCase
,
}
from
'
../../lib/utils/text_utility
'
;
import
*
as
constants
from
'
../constants
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
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
{
name
:
'
CommentForm
'
,
components
:
{
issueWarning
,
noteSignedOutWidget
,
discussionLockedWidget
,
markdownField
,
userAvatarLink
,
loadingButton
,
export
default
{
name
:
'
CommentForm
'
,
components
:
{
issueWarning
,
noteSignedOutWidget
,
discussionLockedWidget
,
markdownField
,
userAvatarLink
,
loadingButton
,
},
mixins
:
[
issuableStateMixin
],
props
:
{
noteableType
:
{
type
:
String
,
required
:
true
,
},
mixins
:
[
issuableStateMixin
,
],
props
:
{
noteableType
:
{
type
:
String
,
required
:
true
,
},
},
data
()
{
return
{
note
:
''
,
noteType
:
constants
.
COMMENT
,
isSubmitting
:
false
,
isSubmitButtonDisabled
:
true
,
};
},
computed
:
{
...
mapGetters
([
'
getCurrentUserLastNote
'
,
'
getUserData
'
,
'
getNoteableData
'
,
'
getNotesData
'
,
'
openState
'
,
]),
...
mapState
([
'
isToggleStateButtonLoading
'
]),
noteableDisplayName
()
{
return
this
.
noteableType
.
replace
(
/_/g
,
'
'
);
},
data
()
{
return
{
note
:
''
,
noteType
:
constants
.
COMMENT
,
isSubmitting
:
false
,
isSubmitButtonDisabled
:
true
,
}
;
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
}
,
commentButtonTitle
()
{
return
this
.
noteType
===
constants
.
COMMENT
?
'
Comment
'
:
'
Start discussion
'
;
},
computed
:
{
...
mapGetters
([
'
getCurrentUserLastNote
'
,
'
getUserData
'
,
'
getNoteableData
'
,
'
getNotesData
'
,
'
openState
'
,
]),
...
mapState
([
'
isToggleStateButtonLoading
'
,
]),
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
'
;
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
)
{
return
sprintf
(
__
(
'
%{actionText} & %{openOrClose} %{noteable}
'
),
{
actionText
:
this
.
commentButtonTitle
,
openOrClose
,
noteable
:
this
.
noteableDisplayName
,
},
);
}
if
(
this
.
note
.
length
)
{
return
sprintf
(
__
(
'
%{actionText} & %{openOrClose} %{noteable}
'
),
{
actionText
:
this
.
commentButtonTitle
,
openOrClose
,
noteable
:
this
.
noteableDisplayName
,
});
}
return
sprintf
(
__
(
'
%{openOrClose} %{noteable}
'
),
{
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
;
},
return
sprintf
(
__
(
'
%{openOrClose} %{noteable}
'
),
{
openOrClose
:
capitalizeFirstCharacter
(
openOrClose
),
noteable
:
this
.
noteableDisplayName
,
});
},
watch
:
{
note
(
newNote
)
{
this
.
setIsSubmitButtonDisabled
(
newNote
,
this
.
isSubmitting
);
}
,
isSubmitting
(
newValue
)
{
this
.
setIsSubmitButtonDisabled
(
this
.
note
,
newValue
);
}
,
actionButtonClassNames
()
{
return
{
'
btn-reopen
'
:
!
this
.
isOpen
,
'
btn-close
'
:
this
.
isOpen
,
'
js-note-target-close
'
:
this
.
isOpen
,
'
js-note-target-reopen
'
:
!
this
.
isOpen
,
}
;
},
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
);
});
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
:
{
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
.
initTaskList
();
this
.
initAutoSave
();
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
:
{
...
mapActions
([
'
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
;
handleSave
(
withIssueAction
)
{
this
.
isSubmitting
=
true
;
if
(
this
.
note
.
length
)
{
const
noteData
=
{
endpoint
:
this
.
endpoint
,
flashContainer
:
this
.
$el
,
data
:
{
note
:
{
noteable_type
:
this
.
noteableType
,
noteable_id
:
this
.
getNoteableData
.
id
,
note
:
this
.
note
,
},
if
(
this
.
note
.
length
)
{
const
noteData
=
{
endpoint
:
this
.
endpoint
,
flashContainer
:
this
.
$el
,
data
:
{
note
:
{
noteable_type
:
this
.
noteableType
,
noteable_id
:
this
.
getNoteableData
.
id
,
note
:
this
.
note
,
},
};
},
};
if
(
this
.
noteType
===
constants
.
DISCUSSION
)
{
noteData
.
data
.
note
.
type
=
constants
.
DISCUSSION_NOTE
;
}
if
(
this
.
noteType
===
constants
.
DISCUSSION
)
{
noteData
.
data
.
note
.
type
=
constants
.
DISCUSSION_NOTE
;
}
this
.
note
=
''
;
// Empty textarea while being requested. Repopulate in catch
this
.
resizeTextarea
();
this
.
stopPolling
();
this
.
note
=
''
;
// Empty textarea while being requested. Repopulate in catch
this
.
resizeTextarea
();
this
.
stopPolling
();
this
.
saveNote
(
noteData
)
.
then
((
res
)
=>
{
this
.
enableButton
();
this
.
restartPolling
();
this
.
saveNote
(
noteData
)
.
then
(
res
=>
{
this
.
enableButton
();
this
.
restartPolling
();
if
(
res
.
errors
)
{
if
(
res
.
errors
.
commands_only
)
{
this
.
discard
();
}
else
{
Flash
(
'
Something went wrong while adding your comment. Please try again.
'
,
'
alert
'
,
this
.
$refs
.
commentForm
,
);
}
}
else
{
if
(
res
.
errors
)
{
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
();
}
if
(
withIssueAction
)
{
this
.
toggleIssueState
();
}
})
.
catch
(()
=>
{
this
.
enableButton
();
this
.
discard
(
false
);
const
msg
=
`Your comment could not be submitted!
if
(
withIssueAction
)
{
this
.
toggleIssueState
();
}
})
.
catch
(()
=>
{
this
.
enableButton
();
this
.
discard
(
false
);
const
msg
=
`Your comment could not be submitted!
Please check your network connection and try again.`
;
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
note
=
noteData
.
data
.
note
.
note
;
// Restore textarea content.
this
.
removePlaceholderNotes
();
});
}
else
{
this
.
toggleIssueState
();
}
},
enableButton
()
{
this
.
isSubmitting
=
false
;
},
toggleIssueState
()
{
if
(
this
.
isOpen
)
{
this
.
closeIssue
()
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
this
.
enableButton
();
this
.
toggleStateButtonLoading
(
false
);
Flash
(
sprintf
(
__
(
'
Something went wrong while closing the %{issuable}. Please try again later
'
),
{
issuable
:
this
.
noteableDisplayName
}
,
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
note
=
noteData
.
data
.
note
.
note
;
// Restore textarea content.
this
.
removePlaceholderNotes
();
});
}
else
{
this
.
toggleIssueState
();
}
},
enableButton
()
{
this
.
isSubmitting
=
false
;
},
toggleIssueState
()
{
if
(
this
.
isOpen
)
{
this
.
closeIssue
()
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
this
.
enableButton
();
this
.
toggleStateButtonLoading
(
false
);
Flash
(
sprintf
(
__
(
'
Something went wrong while closing the %{issuable}. Please try again later
'
,
),
);
});
}
else
{
this
.
reopenIssue
()
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
this
.
enableButton
();
this
.
toggleStateButtonLoading
(
false
);
Flash
(
sprintf
(
__
(
'
Something went wrong while reopening the %{issuable}. Please try again later
'
),
{
issuable
:
this
.
noteableDisplayName
},
{
issuable
:
this
.
noteableDisplayName
},
),
);
});
}
else
{
this
.
reopenIssue
()
.
then
(()
=>
this
.
enableButton
())
.
catch
(()
=>
{
this
.
enableButton
();
this
.
toggleStateButtonLoading
(
false
);
Flash
(
sprintf
(
__
(
'
Something went wrong while reopening the %{issuable}. Please try again later
'
,
),
);
});
}
},
discard
(
shouldClear
=
true
)
{
// `blur` is needed to clear slash commands autocomplete cache if event fired.
// `focus` is needed to remain cursor in the textarea.
this
.
$refs
.
textarea
.
blur
();
this
.
$refs
.
textarea
.
focus
();
{
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.
this
.
$refs
.
textarea
.
blur
();
this
.
$refs
.
textarea
.
focus
();
if
(
shouldClear
)
{
this
.
note
=
''
;
this
.
resizeTextarea
();
this
.
$refs
.
markdownField
.
previewMarkdown
=
false
;
}
if
(
shouldClear
)
{
this
.
note
=
''
;
this
.
resizeTextarea
();
this
.
$refs
.
markdownField
.
previewMarkdown
=
false
;
}
this
.
autosave
.
reset
();
},
setNoteType
(
type
)
{
this
.
noteType
=
type
;
},
editCurrentUserLastNote
()
{
if
(
this
.
note
===
''
)
{
const
lastNote
=
this
.
getCurrentUserLastNote
;
this
.
autosave
.
reset
();
},
setNoteType
(
type
)
{
this
.
noteType
=
type
;
},
editCurrentUserLastNote
()
{
if
(
this
.
note
===
''
)
{
const
lastNote
=
this
.
getCurrentUserLastNote
;
if
(
lastNote
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
noteId
:
lastNote
.
id
,
});
}
if
(
lastNote
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
noteId
:
lastNote
.
id
,
});
}
},
initAutoSave
()
{
if
(
this
.
isLoggedIn
)
{
const
noteableType
=
capitalizeFirstCharacter
(
convertToCamelCase
(
this
.
noteableType
));
}
},
initAutoSave
()
{
if
(
this
.
isLoggedIn
)
{
const
noteableType
=
capitalizeFirstCharacter
(
convertToCamelCase
(
this
.
noteableType
),
);
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
textarea
)
,
[
'
Note
'
,
noteableType
,
this
.
getNoteableData
.
id
]
,
);
}
}
,
initTaskList
()
{
return
new
TaskList
(
{
dataType
:
'
note
'
,
fieldNam
e
:
'
note
'
,
selector
:
'
.notes
'
,
});
}
,
resizeTextarea
()
{
this
.
$nextTick
(()
=>
{
Autosize
.
update
(
this
.
$refs
.
textarea
);
}
);
}
,
this
.
autosave
=
new
Autosave
(
$
(
this
.
$refs
.
textarea
),
[
'
Note
'
,
noteableType
,
this
.
getNoteableData
.
id
,
]);
}
},
initTaskList
()
{
return
new
TaskList
({
dataTyp
e
:
'
note
'
,
fieldName
:
'
note
'
,
selector
:
'
.notes
'
,
}
);
},
resizeTextarea
()
{
this
.
$nextTick
(()
=>
{
Autosize
.
update
(
this
.
$refs
.
textarea
);
}
);
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/diff_file_header.vue
View file @
88bd9bac
<
script
>
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
export
default
{
components
:
{
ClipboardButton
,
Icon
,
export
default
{
components
:
{
ClipboardButton
,
Icon
,
},
props
:
{
diffFile
:
{
type
:
Object
,
required
:
true
,
},
props
:
{
diffFile
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
titleTag
()
{
return
this
.
diffFile
.
discussionPath
?
'
a
'
:
'
span
'
;
},
computed
:
{
titleTag
()
{
return
this
.
diffFile
.
discussionPath
?
'
a
'
:
'
span
'
;
},
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/diff_with_note.vue
View file @
88bd9bac
<
script
>
import
$
from
'
jquery
'
;
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
imageDiffHelper
from
'
~/image_diff/helpers/index
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
DiffFileHeader
from
'
./diff_file_header.vue
'
;
import
$
from
'
jquery
'
;
import
syntaxHighlight
from
'
~/syntax_highlight
'
;
import
imageDiffHelper
from
'
~/image_diff/helpers/index
'
;
import
{
convertObjectPropsToCamelCase
}
from
'
~/lib/utils/common_utils
'
;
import
DiffFileHeader
from
'
./diff_file_header.vue
'
;
export
default
{
components
:
{
DiffFileHeader
,
export
default
{
components
:
{
DiffFileHeader
,
},
props
:
{
discussion
:
{
type
:
Object
,
required
:
true
,
},
props
:
{
discussion
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
isImageDiff
()
{
return
!
this
.
diffFile
.
text
;
},
computed
:
{
isImageDiff
()
{
return
!
this
.
diffFile
.
text
;
},
diffFileClass
()
{
const
{
text
}
=
this
.
diffFile
;
return
text
?
'
text-file
'
:
'
js-image-file
'
;
},
diffRows
()
{
return
$
(
this
.
discussion
.
truncatedDiffLines
);
},
diffFile
()
{
return
convertObjectPropsToCamelCase
(
this
.
discussion
.
diffFile
);
},
imageDiffHtml
()
{
return
this
.
discussion
.
imageDiffHtml
;
},
diffFileClass
()
{
const
{
text
}
=
this
.
diffFile
;
return
text
?
'
text-file
'
:
'
js-image-file
'
;
},
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
);
});
}
diffRows
()
{
return
$
(
this
.
discussion
.
truncatedDiffLines
);
},
methods
:
{
rowTag
(
html
)
{
return
html
.
outerHTML
?
'
tr
'
:
'
template
'
;
},
diffFile
()
{
return
convertObjectPropsToCamelCase
(
this
.
discussion
.
diffFile
);
},
};
imageDiffHtml
()
{
return
this
.
discussion
.
imageDiffHtml
;
},
},
mounted
()
{
if
(
this
.
isImageDiff
)
{
const
canCreateNote
=
false
;
const
renderCommentBadge
=
true
;
imageDiffHelper
.
initImageDiff
(
this
.
$refs
.
fileHolder
,
canCreateNote
,
renderCommentBadge
,
);
}
else
{
const
fileHolder
=
$
(
this
.
$refs
.
fileHolder
);
this
.
$nextTick
(()
=>
{
syntaxHighlight
(
fileHolder
);
});
}
},
methods
:
{
rowTag
(
html
)
{
return
html
.
outerHTML
?
'
tr
'
:
'
template
'
;
},
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/discussion_counter.vue
View file @
88bd9bac
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
resolveSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
mrIssueSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionSvg
from
'
icons/_next_discussion.svg
'
;
import
{
pluralize
}
from
'
../../lib/utils/text_utility
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
{
mapGetters
}
from
'
vuex
'
;
import
resolveSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
mrIssueSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionSvg
from
'
icons/_next_discussion.svg
'
;
import
{
pluralize
}
from
'
../../lib/utils/text_utility
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
export
default
{
directives
:
{
tooltip
,
export
default
{
directives
:
{
tooltip
,
},
computed
:
{
...
mapGetters
([
'
getUserData
'
,
'
getNoteableData
'
,
'
discussionCount
'
,
'
unresolvedDiscussions
'
,
'
resolvedDiscussionCount
'
,
]),
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
computed
:
{
...
mapGetters
([
'
getUserData
'
,
'
getNoteableData
'
,
'
discussionCount
'
,
'
unresolvedDiscussions
'
,
'
resolvedDiscussionCount
'
,
]),
isLoggedIn
()
{
return
this
.
getUserData
.
id
;
},
hasNextButton
()
{
return
this
.
isLoggedIn
&&
!
this
.
allResolved
;
},
countText
()
{
return
pluralize
(
'
discussion
'
,
this
.
discussionCount
);
},
allResolved
()
{
return
this
.
resolvedDiscussionCount
===
this
.
discussionCount
;
},
resolveAllDiscussionsIssuePath
()
{
return
this
.
getNoteableData
.
create_issue_to_resolve_discussions_path
;
},
firstUnresolvedDiscussionId
()
{
const
item
=
this
.
unresolvedDiscussions
[
0
]
||
{};
return
item
.
id
;
},
hasNextButton
()
{
return
this
.
isLoggedIn
&&
!
this
.
allResolved
;
},
countText
()
{
return
pluralize
(
'
discussion
'
,
this
.
discussionCount
);
},
allResolved
()
{
return
this
.
resolvedDiscussionCount
===
this
.
discussionCount
;
},
created
()
{
this
.
resolveSvg
=
resolveSvg
;
this
.
resolvedSvg
=
resolvedSvg
;
this
.
mrIssueSvg
=
mrIssueSvg
;
this
.
nextDiscussionSvg
=
nextDiscussionSvg
;
resolveAllDiscussionsIssuePath
()
{
return
this
.
getNoteableData
.
create_issue_to_resolve_discussions_path
;
},
firstUnresolvedDiscussionId
()
{
const
item
=
this
.
unresolvedDiscussions
[
0
]
||
{};
return
item
.
id
;
},
methods
:
{
jumpToFirstDiscussion
()
{
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
this
.
firstUnresolvedDiscussionId
}
"]`
);
const
activeTab
=
window
.
mrTabs
.
currentAction
;
},
created
()
{
this
.
resolveSvg
=
resolveSvg
;
this
.
resolvedSvg
=
resolvedSvg
;
this
.
mrIssueSvg
=
mrIssueSvg
;
this
.
nextDiscussionSvg
=
nextDiscussionSvg
;
},
methods
:
{
jumpToFirstDiscussion
()
{
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
this
.
firstUnresolvedDiscussionId
}
"]`
,
);
const
activeTab
=
window
.
mrTabs
.
currentAction
;
if
(
activeTab
===
'
commits
'
||
activeTab
===
'
pipelines
'
)
{
window
.
mrTabs
.
activateTab
(
'
show
'
);
}
if
(
activeTab
===
'
commits
'
||
activeTab
===
'
pipelines
'
)
{
window
.
mrTabs
.
activateTab
(
'
show
'
);
}
if
(
el
)
{
scrollToElement
(
el
);
}
},
if
(
el
)
{
scrollToElement
(
el
);
}
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/discussion_locked_widget.vue
View file @
88bd9bac
<
script
>
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Issuable
from
'
~/vue_shared/mixins/issuable
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Issuable
from
'
~/vue_shared/mixins/issuable
'
;
export
default
{
components
:
{
Icon
,
},
mixins
:
[
Issuable
,
],
};
export
default
{
components
:
{
Icon
,
},
mixins
:
[
Issuable
],
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_actions.vue
View file @
88bd9bac
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
editSvg
from
'
icons/_icon_pencil.svg
'
;
import
resolveDiscussionSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedDiscussionSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
ellipsisSvg
from
'
icons/_ellipsis_v.svg
'
;
import
loadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
import
{
mapGetters
}
from
'
vuex
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
editSvg
from
'
icons/_icon_pencil.svg
'
;
import
resolveDiscussionSvg
from
'
icons/_icon_resolve_discussion.svg
'
;
import
resolvedDiscussionSvg
from
'
icons/_icon_status_success_solid.svg
'
;
import
ellipsisSvg
from
'
icons/_ellipsis_v.svg
'
;
import
loadingIcon
from
'
~/vue_shared/components/loading_icon.vue
'
;
import
tooltip
from
'
~/vue_shared/directives/tooltip
'
;
export
default
{
name
:
'
NoteActions
'
,
directives
:
{
tooltip
,
},
components
:
{
loadingIcon
,
},
props
:
{
authorId
:
{
type
:
Number
,
required
:
true
,
},
noteId
:
{
type
:
Number
,
required
:
true
,
},
accessLevel
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
reportAbusePath
:
{
type
:
String
,
required
:
true
,
},
canEdit
:
{
type
:
Boolean
,
required
:
true
,
},
canDelete
:
{
type
:
Boolean
,
required
:
true
,
},
resolvable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isResolved
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isResolving
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
resolvedBy
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
canReportAsAbuse
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
...
mapGetters
([
'
getUserDataByProp
'
,
]),
shouldShowActionsDropdown
()
{
return
this
.
currentUserId
&&
(
this
.
canEdit
||
this
.
canReportAsAbuse
);
},
canAddAwardEmoji
()
{
return
this
.
currentUserId
;
},
isAuthoredByCurrentUser
()
{
return
this
.
authorId
===
this
.
currentUserId
;
},
currentUserId
()
{
return
this
.
getUserDataByProp
(
'
id
'
);
},
resolveButtonTitle
()
{
let
title
=
'
Mark as resolved
'
;
export
default
{
name
:
'
NoteActions
'
,
directives
:
{
tooltip
,
},
components
:
{
loadingIcon
,
},
props
:
{
authorId
:
{
type
:
Number
,
required
:
true
,
},
noteId
:
{
type
:
Number
,
required
:
true
,
},
accessLevel
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
reportAbusePath
:
{
type
:
String
,
required
:
true
,
},
canEdit
:
{
type
:
Boolean
,
required
:
true
,
},
canDelete
:
{
type
:
Boolean
,
required
:
true
,
},
resolvable
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isResolved
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
isResolving
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
resolvedBy
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
canReportAsAbuse
:
{
type
:
Boolean
,
required
:
true
,
},
},
computed
:
{
...
mapGetters
([
'
getUserDataByProp
'
]),
shouldShowActionsDropdown
()
{
return
this
.
currentUserId
&&
(
this
.
canEdit
||
this
.
canReportAsAbuse
);
},
canAddAwardEmoji
()
{
return
this
.
currentUserId
;
},
isAuthoredByCurrentUser
()
{
return
this
.
authorId
===
this
.
currentUserId
;
},
currentUserId
()
{
return
this
.
getUserDataByProp
(
'
id
'
);
},
resolveButtonTitle
()
{
let
title
=
'
Mark as resolved
'
;
if
(
this
.
resolvedBy
)
{
title
=
`Resolved by
${
this
.
resolvedBy
.
name
}
`
;
}
if
(
this
.
resolvedBy
)
{
title
=
`Resolved by
${
this
.
resolvedBy
.
name
}
`
;
}
return
title
;
},
},
created
()
{
this
.
emojiSmiling
=
emojiSmiling
;
this
.
emojiSmile
=
emojiSmile
;
this
.
emojiSmiley
=
emojiSmiley
;
this
.
editSvg
=
editSvg
;
this
.
ellipsisSvg
=
ellipsisSvg
;
this
.
resolveDiscussionSvg
=
resolveDiscussionSvg
;
this
.
resolvedDiscussionSvg
=
resolvedDiscussionSvg
;
},
methods
:
{
onEdit
()
{
this
.
$emit
(
'
handleEdit
'
);
},
onDelete
()
{
this
.
$emit
(
'
handleDelete
'
);
},
onResolve
()
{
this
.
$emit
(
'
handleResolve
'
);
},
},
};
return
title
;
},
},
created
()
{
this
.
emojiSmiling
=
emojiSmiling
;
this
.
emojiSmile
=
emojiSmile
;
this
.
emojiSmiley
=
emojiSmiley
;
this
.
editSvg
=
editSvg
;
this
.
ellipsisSvg
=
ellipsisSvg
;
this
.
resolveDiscussionSvg
=
resolveDiscussionSvg
;
this
.
resolvedDiscussionSvg
=
resolvedDiscussionSvg
;
},
methods
:
{
onEdit
()
{
this
.
$emit
(
'
handleEdit
'
);
},
onDelete
()
{
this
.
$emit
(
'
handleDelete
'
);
},
onResolve
()
{
this
.
$emit
(
'
handleResolve
'
);
},
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_attachment.vue
View file @
88bd9bac
<
script
>
export
default
{
name
:
'
NoteAttachment
'
,
props
:
{
attachment
:
{
type
:
Object
,
required
:
true
,
},
export
default
{
name
:
'
NoteAttachment
'
,
props
:
{
attachment
:
{
type
:
Object
,
required
:
true
,
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_awards_list.vue
View file @
88bd9bac
<
script
>
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
Flash
from
'
../../flash
'
;
import
{
glEmojiTag
}
from
'
../../emoji
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
export
default
{
directives
:
{
tooltip
,
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
Flash
from
'
../../flash
'
;
import
{
glEmojiTag
}
from
'
../../emoji
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
export
default
{
directives
:
{
tooltip
,
},
props
:
{
awards
:
{
type
:
Array
,
required
:
true
,
},
props
:
{
awards
:
{
type
:
Array
,
required
:
true
,
},
toggleAwardPath
:
{
type
:
String
,
required
:
true
,
},
noteAuthorId
:
{
type
:
Number
,
required
:
true
,
},
noteId
:
{
type
:
Number
,
required
:
true
,
},
toggleAwardPath
:
{
type
:
String
,
required
:
true
,
},
computed
:
{
...
mapGetters
([
'
getUserData
'
,
]),
// `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
;
},
noteAuthorId
:
{
type
:
Number
,
required
:
true
,
},
created
()
{
this
.
emojiSmiling
=
emojiSmiling
;
this
.
emojiSmile
=
emojiSmile
;
this
.
emojiSmiley
=
emojiSmiley
;
noteId
:
{
type
:
Number
,
required
:
true
,
},
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
;
},
computed
:
{
...
mapGetters
([
'
getUserData
'
]),
// `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
]
});
}
const
data
=
{
endpoint
:
this
.
toggleAwardPath
,
noteId
:
this
.
noteId
,
awardName
:
parsedName
,
};
this
.
toggleAwardRequest
(
data
)
.
catch
(()
=>
Flash
(
'
Something went wrong on our end.
'
));
},
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
()
{
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
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_body.vue
View file @
88bd9bac
<
script
>
import
$
from
'
jquery
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteAwardsList
from
'
./note_awards_list.vue
'
;
import
noteAttachment
from
'
./note_attachment.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
TaskList
from
'
../../task_list
'
;
import
autosave
from
'
../mixins/autosave
'
;
import
$
from
'
jquery
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteAwardsList
from
'
./note_awards_list.vue
'
;
import
noteAttachment
from
'
./note_attachment.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
TaskList
from
'
../../task_list
'
;
import
autosave
from
'
../mixins/autosave
'
;
export
default
{
components
:
{
noteEditedText
,
noteAwardsList
,
noteAttachment
,
noteForm
,
export
default
{
components
:
{
noteEditedText
,
noteAwardsList
,
noteAttachment
,
noteForm
,
},
mixins
:
[
autosave
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
mixins
:
[
autosave
,
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
canEdit
:
{
type
:
Boolean
,
required
:
true
,
},
isEditing
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
canEdit
:
{
type
:
Boolean
,
required
:
true
,
},
computed
:
{
noteBody
()
{
return
this
.
note
.
note
;
}
,
isEditing
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
mounted
()
{
this
.
renderGFM
();
this
.
initTaskList
();
},
computed
:
{
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
);
}
else
{
this
.
setAutoSave
();
}
}
},
methods
:
{
renderGFM
()
{
$
(
this
.
$refs
[
'
note-body
'
]).
renderGFM
();
},
updated
()
{
this
.
initTaskList
();
this
.
renderGFM
();
if
(
this
.
isEditing
)
{
if
(
!
this
.
autosave
)
{
this
.
initAutoSave
(
this
.
note
.
noteable_type
);
}
else
{
this
.
setAutoSave
();
}
initTaskList
()
{
if
(
this
.
canEdit
)
{
this
.
taskList
=
new
TaskList
({
dataType
:
'
note
'
,
fieldName
:
'
note
'
,
selector
:
'
.notes
'
,
});
}
},
methods
:
{
renderGFM
()
{
$
(
this
.
$refs
[
'
note-body
'
]).
renderGFM
();
},
initTaskList
()
{
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
);
},
handleFormUpdate
(
note
,
parentElement
,
callback
)
{
this
.
$emit
(
'
handleFormUpdate
'
,
note
,
parentElement
,
callback
);
},
formCancelHandler
(
shouldConfirm
,
isDirty
)
{
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
isDirty
);
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_edited_text.vue
View file @
88bd9bac
<
script
>
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
export
default
{
name
:
'
EditedNoteText
'
,
components
:
{
timeAgoTooltip
,
export
default
{
name
:
'
EditedNoteText
'
,
components
:
{
timeAgoTooltip
,
},
props
:
{
actionText
:
{
type
:
String
,
required
:
true
,
},
props
:
{
actionText
:
{
type
:
String
,
required
:
true
,
},
editedAt
:
{
type
:
String
,
required
:
true
,
},
editedBy
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
className
:
{
type
:
String
,
required
:
false
,
default
:
'
edited-text
'
,
},
editedAt
:
{
type
:
String
,
required
:
true
,
},
};
editedBy
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
className
:
{
type
:
String
,
required
:
false
,
default
:
'
edited-text
'
,
},
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_form.vue
View file @
88bd9bac
<
script
>
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueWarning
from
'
../../vue_shared/components/issue/issue_warning.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
issuableStateMixin
from
'
../mixins/issuable_state
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
export
default
{
name
:
'
IssueNoteForm
'
,
components
:
{
issueWarning
,
markdownField
,
export
default
{
name
:
'
IssueNoteForm
'
,
components
:
{
issueWarning
,
markdownField
,
},
mixins
:
[
issuableStateMixin
,
resolvable
],
props
:
{
noteBody
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
mixins
:
[
issuableStateMixin
,
resolvable
,
],
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
,
},
noteId
:
{
type
:
Number
,
required
:
false
,
default
:
0
,
},
data
()
{
return
{
updatedNoteBody
:
this
.
noteBody
,
conflictWhileEditing
:
false
,
isSubmitting
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
};
saveButtonTitle
:
{
type
:
String
,
required
:
false
,
default
:
'
Save comment
'
,
},
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
;
},
currentUserId
()
{
return
this
.
getUserDataByProp
(
'
id
'
);
},
isDisabled
()
{
return
!
this
.
updatedNoteBody
.
length
||
this
.
isSubmitting
;
},
note
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
watch
:
{
noteBody
()
{
if
(
this
.
updatedNoteBody
===
this
.
noteBody
)
{
this
.
updatedNoteBody
=
this
.
noteBody
;
}
else
{
this
.
conflictWhileEditing
=
true
;
}
},
isEditing
:
{
type
:
Boolean
,
required
:
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
()
{
this
.
$refs
.
textarea
.
focus
(
);
currentUserI
d
()
{
return
this
.
getUserDataByProp
(
'
id
'
);
},
methods
:
{
...
mapActions
([
'
toggleResolveNote
'
,
]),
handleUpdate
(
shouldResolve
)
{
const
beforeSubmitDiscussionState
=
this
.
discussionResolved
;
this
.
isSubmitting
=
true
;
isDisabled
()
{
return
!
this
.
updatedNoteBody
.
length
||
this
.
isSubmitting
;
},
},
watch
:
{
noteBody
()
{
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
;
if
(
shouldResolve
)
{
this
.
resolveHandler
(
beforeSubmitDiscussionState
);
}
});
},
editMyLastNote
()
{
if
(
this
.
updatedNoteBody
===
''
)
{
const
lastNoteInDiscussion
=
this
.
getDiscussionLastNote
(
this
.
updatedNoteBody
);
},
);
},
editMyLastNote
()
{
if
(
this
.
updatedNoteBody
===
''
)
{
const
lastNoteInDiscussion
=
this
.
getDiscussionLastNote
(
this
.
updatedNoteBody
,
);
if
(
lastNoteInDiscussion
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
noteId
:
lastNoteInDiscussion
.
id
,
});
}
if
(
lastNoteInDiscussion
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
noteId
:
lastNoteInDiscussion
.
id
,
});
}
},
cancelHandler
(
shouldConfirm
=
false
)
{
// Sends information about confirm message and if the textarea has changed
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
this
.
noteBody
!==
this
.
updatedNoteBody
);
},
}
},
cancelHandler
(
shouldConfirm
=
false
)
{
// Sends information about confirm message and if the textarea has changed
this
.
$emit
(
'
cancelFormEdition
'
,
shouldConfirm
,
this
.
noteBody
!==
this
.
updatedNoteBody
,
);
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_header.vue
View file @
88bd9bac
<
script
>
import
{
mapActions
}
from
'
vuex
'
;
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
import
{
mapActions
}
from
'
vuex
'
;
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
export
default
{
components
:
{
timeAgoTooltip
,
export
default
{
components
:
{
timeAgoTooltip
,
},
props
:
{
author
:
{
type
:
Object
,
required
:
true
,
},
props
:
{
author
:
{
type
:
Object
,
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
,
},
createdAt
:
{
type
:
String
,
required
:
true
,
},
computed
:
{
toggleChevronClass
()
{
return
this
.
expanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
},
noteTimestampLink
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
actionText
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
methods
:
{
...
mapActions
([
'
setTargetNoteHash
'
,
]),
handleToggle
()
{
this
.
$emit
(
'
toggleHandler
'
);
},
updateTargetNoteHash
()
{
this
.
setTargetNoteHash
(
this
.
noteTimestampLink
);
},
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
:
{
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
>
<
template
>
...
...
app/assets/javascripts/notes/components/note_signed_out_widget.vue
View file @
88bd9bac
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
mapGetters
}
from
'
vuex
'
;
export
default
{
computed
:
{
...
mapGetters
([
'
getNotesDataByProp
'
,
]),
registerLink
()
{
return
this
.
getNotesDataByProp
(
'
registerPath
'
);
},
signInLink
()
{
return
this
.
getNotesDataByProp
(
'
newSessionPath
'
);
},
export
default
{
computed
:
{
...
mapGetters
([
'
getNotesDataByProp
'
]),
registerLink
()
{
return
this
.
getNotesDataByProp
(
'
registerPath
'
);
},
};
signInLink
()
{
return
this
.
getNotesDataByProp
(
'
newSessionPath
'
);
},
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/noteable_discussion.vue
View file @
88bd9bac
<
script
>
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
resolveDiscussionsSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionsSvg
from
'
icons/_next_discussion.svg
'
;
import
Flash
from
'
../../flash
'
;
import
{
SYSTEM_NOTE
}
from
'
../constants
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
noteableNote
from
'
./noteable_note.vue
'
;
import
noteHeader
from
'
./note_header.vue
'
;
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
diffWithNote
from
'
./diff_with_note.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
autosave
from
'
../mixins/autosave
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
import
{
mapActions
,
mapGetters
}
from
'
vuex
'
;
import
resolveDiscussionsSvg
from
'
icons/_icon_mr_issue.svg
'
;
import
nextDiscussionsSvg
from
'
icons/_next_discussion.svg
'
;
import
Flash
from
'
../../flash
'
;
import
{
SYSTEM_NOTE
}
from
'
../constants
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
noteableNote
from
'
./noteable_note.vue
'
;
import
noteHeader
from
'
./note_header.vue
'
;
import
noteSignedOutWidget
from
'
./note_signed_out_widget.vue
'
;
import
noteEditedText
from
'
./note_edited_text.vue
'
;
import
noteForm
from
'
./note_form.vue
'
;
import
diffWithNote
from
'
./diff_with_note.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
autosave
from
'
../mixins/autosave
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
tooltip
from
'
../../vue_shared/directives/tooltip
'
;
import
{
scrollToElement
}
from
'
../../lib/utils/common_utils
'
;
export
default
{
components
:
{
noteableNote
,
diffWithNote
,
userAvatarLink
,
noteHeader
,
noteSignedOutWidget
,
noteEditedText
,
noteForm
,
placeholderNote
,
placeholderSystemNote
,
export
default
{
components
:
{
noteableNote
,
diffWithNote
,
userAvatarLink
,
noteHeader
,
noteSignedOutWidget
,
noteEditedText
,
noteForm
,
placeholderNote
,
placeholderSystemNote
,
},
directives
:
{
tooltip
,
},
mixins
:
[
autosave
,
noteable
,
resolvable
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
directives
:
{
tooltip
,
},
mixins
:
[
autosave
,
noteable
,
resolvable
,
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
},
data
()
{
return
{
isReplying
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
};
},
computed
:
{
...
mapGetters
([
'
getNoteableData
'
,
'
discussionCount
'
,
'
resolvedDiscussionCount
'
,
'
unresolvedDiscussions
'
,
]),
discussion
()
{
return
{
isReplying
:
false
,
isResolving
:
false
,
resolveAsThread
:
true
,
...
this
.
note
.
notes
[
0
],
truncatedDiffLines
:
this
.
note
.
truncated_diff_lines
,
diffFile
:
this
.
note
.
diff_file
,
diffDiscussion
:
this
.
note
.
diff_discussion
,
imageDiffHtml
:
this
.
note
.
image_diff_html
,
};
},
computed
:
{
...
mapGetters
([
'
getNoteableData
'
,
'
discussionCount
'
,
'
resolvedDiscussionCount
'
,
'
unresolvedDiscussions
'
,
]),
discussion
()
{
return
{
...
this
.
note
.
notes
[
0
],
truncatedDiffLines
:
this
.
note
.
truncated_diff_lines
,
diffFile
:
this
.
note
.
diff_file
,
diffDiscussion
:
this
.
note
.
diff_discussion
,
imageDiffHtml
:
this
.
note
.
image_diff_html
,
};
},
author
()
{
return
this
.
discussion
.
author
;
},
canReply
()
{
return
this
.
getNoteableData
.
current_user
.
can_create_note
;
},
newNotePath
()
{
return
this
.
getNoteableData
.
create_note_path
;
},
lastUpdatedBy
()
{
const
{
notes
}
=
this
.
note
;
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
)
{
return
notes
[
notes
.
length
-
1
].
author
;
}
if
(
notes
.
length
>
1
)
{
return
notes
[
notes
.
length
-
1
].
author
;
}
return
null
;
},
lastUpdatedAt
()
{
const
{
notes
}
=
this
.
note
;
return
null
;
},
lastUpdatedAt
()
{
const
{
notes
}
=
this
.
note
;
if
(
notes
.
length
>
1
)
{
return
notes
[
notes
.
length
-
1
].
created_at
;
}
if
(
notes
.
length
>
1
)
{
return
notes
[
notes
.
length
-
1
].
created_at
;
}
return
null
;
},
hasUnresolvedDiscussion
()
{
return
this
.
unresolvedDiscussions
.
length
>
0
;
},
wrapperComponent
()
{
return
(
this
.
discussion
.
diffDiscussion
&&
this
.
discussion
.
diffFile
)
?
diffWithNote
:
'
div
'
;
},
wrapperClass
()
{
return
this
.
isDiffDiscussion
?
''
:
'
panel panel-default
'
;
},
return
null
;
},
hasUnresolvedDiscussion
()
{
return
this
.
unresolvedDiscussions
.
length
>
0
;
},
wrapperComponent
()
{
return
this
.
discussion
.
diffDiscussion
&&
this
.
discussion
.
diffFile
?
diffWithNote
:
'
div
'
;
},
mounted
()
{
if
(
this
.
isReplying
)
{
wrapperClass
()
{
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
);
}
else
{
this
.
setAutoSave
();
}
},
updated
()
{
if
(
this
.
isReplying
)
{
if
(
!
this
.
autosave
)
{
this
.
initAutoSave
(
this
.
discussion
.
noteable_type
);
}
else
{
this
.
setAutoSave
();
}
},
created
()
{
this
.
resolveDiscussionsSvg
=
resolveDiscussionsSvg
;
this
.
nextDiscussionsSvg
=
nextDiscussionsSvg
;
},
methods
:
{
...
mapActions
([
'
saveNote
'
,
'
toggleDiscussion
'
,
'
removePlaceholderNotes
'
,
'
toggleResolveNote
'
,
]),
componentName
(
note
)
{
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
}
return
placeholderNote
;
}
return
noteableNote
;
},
created
()
{
this
.
resolveDiscussionsSvg
=
resolveDiscussionsSvg
;
this
.
nextDiscussionsSvg
=
nextDiscussionsSvg
;
componentData
(
note
)
{
return
note
.
isPlaceholderNote
?
this
.
note
.
notes
[
0
]
:
note
;
},
methods
:
{
...
mapActions
([
'
saveNote
'
,
'
toggleDiscussion
'
,
'
removePlaceholderNotes
'
,
'
toggleResolveNote
'
,
]),
componentName
(
note
)
{
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
}
return
placeholderNote
;
}
toggleDiscussionHandler
()
{
this
.
toggleDiscussion
({
discussionId
:
this
.
note
.
id
});
},
showReplyForm
()
{
this
.
isReplying
=
true
;
},
cancelReplyForm
(
shouldConfirm
)
{
if
(
shouldConfirm
&&
this
.
$refs
.
noteForm
.
isDirty
)
{
const
msg
=
'
Are you sure you want to cancel creating this comment?
'
;
return
noteableNote
;
},
componentData
(
note
)
{
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
;
}
// eslint-disable-next-line no-alert
if
(
!
confirm
(
msg
))
{
return
;
}
}
this
.
resetAutoSave
();
this
.
isReplying
=
false
;
},
saveReply
(
noteText
,
form
,
callback
)
{
const
replyData
=
{
endpoint
:
this
.
newNotePath
,
flashContainer
:
this
.
$el
,
data
:
{
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
target_type
:
this
.
noteableType
,
target_id
:
this
.
discussion
.
noteable_id
,
note
:
{
note
:
noteText
},
},
};
this
.
isReplying
=
false
;
this
.
resetAutoSave
();
this
.
isReplying
=
false
;
},
saveReply
(
noteText
,
form
,
callback
)
{
const
replyData
=
{
endpoint
:
this
.
newNotePath
,
flashContainer
:
this
.
$el
,
data
:
{
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
target_type
:
this
.
noteableType
,
target_id
:
this
.
discussion
.
noteable_id
,
note
:
{
note
:
noteText
},
},
};
this
.
isReplying
=
false
;
this
.
saveNote
(
replyData
)
.
then
(()
=>
{
this
.
resetAutoSave
();
callback
();
})
.
catch
((
err
)
=>
{
this
.
removePlaceholderNotes
();
this
.
isReplying
=
true
;
this
.
$nextTick
(()
=>
{
const
msg
=
`Your comment could not be submitted!
this
.
saveNote
(
replyData
)
.
then
(()
=>
{
this
.
resetAutoSave
();
callback
();
})
.
catch
(
err
=>
{
this
.
removePlaceholderNotes
();
this
.
isReplying
=
true
;
this
.
$nextTick
(()
=>
{
const
msg
=
`Your comment could not be submitted!
Please check your network connection and try again.`
;
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
$refs
.
noteForm
.
note
=
noteText
;
callback
(
err
);
});
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
$refs
.
noteForm
.
note
=
noteText
;
callback
(
err
);
});
},
jumpToDiscussion
()
{
const
unresolvedIds
=
this
.
unresolvedDiscussions
.
map
(
d
=>
d
.
id
);
const
index
=
unresolvedIds
.
indexOf
(
this
.
note
.
id
);
});
},
jumpToDiscussion
()
{
const
unresolvedIds
=
this
.
unresolvedDiscussions
.
map
(
d
=>
d
.
id
);
const
index
=
unresolvedIds
.
indexOf
(
this
.
note
.
id
);
if
(
index
>=
0
&&
index
!==
unresolvedIds
.
length
)
{
const
nextId
=
unresolvedIds
[
index
+
1
];
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
nextId
}
"]`
);
if
(
index
>=
0
&&
index
!==
unresolvedIds
.
length
)
{
const
nextId
=
unresolvedIds
[
index
+
1
];
const
el
=
document
.
querySelector
(
`[data-discussion-id="
${
nextId
}
"]`
);
if
(
el
)
{
scrollToElement
(
el
);
}
if
(
el
)
{
scrollToElement
(
el
);
}
}
,
}
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/components/noteable_note.vue
View file @
88bd9bac
<
script
>
import
$
from
'
jquery
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
escape
}
from
'
underscore
'
;
import
Flash
from
'
../../flash
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
noteHeader
from
'
./note_header.vue
'
;
import
noteActions
from
'
./note_actions.vue
'
;
import
noteBody
from
'
./note_body.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
import
$
from
'
jquery
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
escape
}
from
'
underscore
'
;
import
Flash
from
'
../../flash
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
noteHeader
from
'
./note_header.vue
'
;
import
noteActions
from
'
./note_actions.vue
'
;
import
noteBody
from
'
./note_body.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
noteable
from
'
../mixins/noteable
'
;
import
resolvable
from
'
../mixins/resolvable
'
;
export
default
{
components
:
{
userAvatarLink
,
noteHeader
,
noteActions
,
noteBody
,
export
default
{
components
:
{
userAvatarLink
,
noteHeader
,
noteActions
,
noteBody
,
},
mixins
:
[
noteable
,
resolvable
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
mixins
:
[
noteable
,
resolvable
,
],
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
data
()
{
return
{
isEditing
:
false
,
isDeleting
:
false
,
isRequesting
:
false
,
isResolving
:
false
,
};
},
computed
:
{
...
mapGetters
([
'
targetNoteHash
'
,
'
getUserData
'
]),
author
()
{
return
this
.
note
.
author
;
},
data
()
{
classNameBindings
()
{
return
{
isEditing
:
false
,
isDeleting
:
false
,
isRequesting
:
false
,
isResolving
:
false
,
'
is-editing
'
:
this
.
isEditing
&&
!
this
.
isRequesting
,
'
is-requesting being-posted
'
:
this
.
isRequesting
,
'
disabled-content
'
:
this
.
isDeleting
,
target
:
this
.
targetNoteHash
===
this
.
noteAnchorId
,
};
},
computed
:
{
...
mapGetters
([
'
targetNoteHash
'
,
'
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
}
`
;
},
canReportAsAbuse
()
{
return
(
this
.
note
.
report_abuse_path
&&
this
.
author
.
id
!==
this
.
getUserData
.
id
);
},
created
()
{
eventHub
.
$on
(
'
enterEditMode
'
,
({
noteId
})
=>
{
if
(
noteId
===
this
.
note
.
id
)
{
this
.
isEditing
=
true
;
this
.
scrollToNoteIfNeeded
(
$
(
this
.
$el
));
}
});
noteAnchorId
()
{
return
`note_
${
this
.
note
.
id
}
`
;
},
},
methods
:
{
...
mapActions
([
'
deleteNote
'
,
'
updateNote
'
,
'
toggleResolveNote
'
,
'
scrollToNoteIfNeeded
'
,
]),
editHandler
()
{
created
()
{
eventHub
.
$on
(
'
enterEditMode
'
,
({
noteId
})
=>
{
if
(
noteId
===
this
.
note
.
id
)
{
this
.
isEditing
=
true
;
},
deleteHandler
()
{
// eslint-disable-next-line no-alert
if
(
confirm
(
'
Are you sure you want to delete this comment?
'
))
{
this
.
isDeleting
=
true
;
this
.
scrollToNoteIfNeeded
(
$
(
this
.
$el
));
}
});
},
this
.
deleteNote
(
this
.
note
)
.
then
(()
=>
{
this
.
isDeleting
=
false
;
})
.
catch
(()
=>
{
Flash
(
'
Something went wrong while deleting your note. Please try again.
'
);
this
.
isDeleting
=
false
;
});
}
},
formUpdateHandler
(
noteText
,
parentElement
,
callback
)
{
const
data
=
{
endpoint
:
this
.
note
.
path
,
note
:
{
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
);
methods
:
{
...
mapActions
([
'
deleteNote
'
,
'
updateNote
'
,
'
toggleResolveNote
'
,
'
scrollToNoteIfNeeded
'
,
]),
editHandler
()
{
this
.
isEditing
=
true
;
},
deleteHandler
()
{
// eslint-disable-next-line no-alert
if
(
confirm
(
'
Are you sure you want to delete this comment?
'
))
{
this
.
isDeleting
=
true
;
this
.
updateNote
(
data
)
this
.
deleteNote
(
this
.
note
)
.
then
(()
=>
{
this
.
isEditing
=
false
;
this
.
isRequesting
=
false
;
this
.
oldContent
=
null
;
$
(
this
.
$refs
.
noteBody
.
$el
).
renderGFM
();
this
.
$refs
.
noteBody
.
resetAutoSave
();
callback
();
this
.
isDeleting
=
false
;
})
.
catch
(()
=>
{
this
.
isRequesting
=
false
;
this
.
isEditing
=
true
;
this
.
$nextTick
(()
=>
{
const
msg
=
'
Something went wrong while editing your comment. Please try again.
'
;
Flash
(
msg
,
'
alert
'
,
this
.
$el
);
this
.
recoverNoteContent
(
noteText
);
callback
();
});
Flash
(
'
Something went wrong while deleting your note. Please try again.
'
,
);
this
.
isDeleting
=
false
;
});
},
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
;
}
},
formUpdateHandler
(
noteText
,
parentElement
,
callback
)
{
const
data
=
{
endpoint
:
this
.
note
.
path
,
note
:
{
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
)
.
then
(()
=>
{
this
.
isEditing
=
false
;
this
.
isRequesting
=
false
;
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
;
},
$
(
this
.
$refs
.
noteBody
.
$el
).
renderGFM
();
this
.
$refs
.
noteBody
.
resetAutoSave
();
callback
();
})
.
catch
(()
=>
{
this
.
isRequesting
=
false
;
this
.
isEditing
=
true
;
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
>
<
template
>
...
...
app/assets/javascripts/notes/components/notes_app.vue
View file @
88bd9bac
<
script
>
import
$
from
'
jquery
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
getLocationHash
}
from
'
../../lib/utils/url_utility
'
;
import
Flash
from
'
../../flash
'
;
import
store
from
'
../stores/
'
;
import
*
as
constants
from
'
../constants
'
;
import
noteableNote
from
'
./noteable_note.vue
'
;
import
noteableDiscussion
from
'
./noteable_discussion.vue
'
;
import
systemNote
from
'
../../vue_shared/components/notes/system_note.vue
'
;
import
commentForm
from
'
./comment_form.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
skeletonLoadingContainer
from
'
../../vue_shared/components/notes/skeleton_note.vue
'
;
import
$
from
'
jquery
'
;
import
{
mapGetters
,
mapActions
}
from
'
vuex
'
;
import
{
getLocationHash
}
from
'
../../lib/utils/url_utility
'
;
import
Flash
from
'
../../flash
'
;
import
store
from
'
../stores/
'
;
import
*
as
constants
from
'
../constants
'
;
import
noteableNote
from
'
./noteable_note.vue
'
;
import
noteableDiscussion
from
'
./noteable_discussion.vue
'
;
import
systemNote
from
'
../../vue_shared/components/notes/system_note.vue
'
;
import
commentForm
from
'
./comment_form.vue
'
;
import
placeholderNote
from
'
../../vue_shared/components/notes/placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
../../vue_shared/components/notes/placeholder_system_note.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
import
skeletonLoadingContainer
from
'
../../vue_shared/components/notes/skeleton_note.vue
'
;
export
default
{
name
:
'
NotesApp
'
,
components
:
{
noteableNote
,
noteableDiscussion
,
systemNote
,
commentForm
,
loadingIcon
,
placeholderNote
,
placeholderSystemNote
,
export
default
{
name
:
'
NotesApp
'
,
components
:
{
noteableNote
,
noteableDiscussion
,
systemNote
,
commentForm
,
loadingIcon
,
placeholderNote
,
placeholderSystemNote
,
},
props
:
{
noteableData
:
{
type
:
Object
,
required
:
true
,
},
props
:
{
noteableData
:
{
type
:
Object
,
required
:
true
,
},
notesData
:
{
type
:
Object
,
required
:
true
,
},
userData
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
notesData
:
{
type
:
Object
,
required
:
true
,
},
store
,
data
()
{
return
{
isLoading
:
true
,
};
userData
:
{
type
:
Object
,
required
:
false
,
default
:
()
=>
({}),
},
computed
:
{
...
mapGetters
([
'
notes
'
,
'
getNotesDataByProp
'
,
'
discussionCount
'
,
]),
noteableType
()
{
// FIXME -- @fatihacet Get this from JSON data.
const
{
ISSUE_NOTEABLE_TYPE
,
MERGE_REQUEST_NOTEABLE_TYPE
}
=
constants
;
},
store
,
data
()
{
return
{
isLoading
:
true
,
};
},
computed
:
{
...
mapGetters
([
'
notes
'
,
'
getNotesDataByProp
'
,
'
discussionCount
'
]),
noteableType
()
{
// FIXME -- @fatihacet Get this from JSON data.
const
{
ISSUE_NOTEABLE_TYPE
,
MERGE_REQUEST_NOTEABLE_TYPE
}
=
constants
;
return
this
.
noteableData
.
merge_params
?
MERGE_REQUEST_NOTEABLE_TYPE
:
ISSUE_NOTEABLE_TYPE
;
},
allNotes
()
{
if
(
this
.
isLoading
)
{
const
totalNotes
=
parseInt
(
this
.
notesData
.
totalNotes
,
10
)
||
0
;
return
new
Array
(
totalNotes
).
fill
({
isSkeletonNote
:
true
,
});
}
return
this
.
notes
;
},
},
created
()
{
this
.
setNotesData
(
this
.
notesData
);
this
.
setNoteableData
(
this
.
noteableData
);
this
.
setUserData
(
this
.
userData
);
return
this
.
noteableData
.
merge_params
?
MERGE_REQUEST_NOTEABLE_TYPE
:
ISSUE_NOTEABLE_TYPE
;
},
mounted
()
{
this
.
fetchNotes
();
allNotes
()
{
if
(
this
.
isLoading
)
{
const
totalNotes
=
parseInt
(
this
.
notesData
.
totalNotes
,
10
)
||
0
;
const
parentElement
=
this
.
$el
.
parentElement
;
if
(
parentElement
&&
parentElement
.
classList
.
contains
(
'
js-vue-notes-event
'
))
{
parentElement
.
addEventListener
(
'
toggleAward
'
,
(
event
)
=>
{
const
{
awardName
,
noteId
}
=
event
.
detail
;
this
.
actionToggleAward
({
awardName
,
noteId
});
return
new
Array
(
totalNotes
).
fill
({
isSkeletonNote
:
true
,
});
}
document
.
addEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
},
beforeDestroy
()
{
document
.
removeEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
return
this
.
notes
;
},
methods
:
{
...
mapActions
({
actionFetchNotes
:
'
fetchNotes
'
,
poll
:
'
poll
'
,
actionToggleAward
:
'
toggleAward
'
,
scrollToNoteIfNeeded
:
'
scrollToNoteIfNeeded
'
,
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
;
}
},
created
()
{
this
.
setNotesData
(
this
.
notesData
);
this
.
setNoteableData
(
this
.
noteableData
);
this
.
setUserData
(
this
.
userData
);
},
mounted
()
{
this
.
fetchNotes
();
const
parentElement
=
this
.
$el
.
parentElement
;
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
;
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
);
},
beforeDestroy
()
{
document
.
removeEventListener
(
'
refreshVueNotes
'
,
this
.
fetchNotes
);
},
methods
:
{
...
mapActions
({
actionFetchNotes
:
'
fetchNotes
'
,
poll
:
'
poll
'
,
actionToggleAward
:
'
toggleAward
'
,
scrollToNoteIfNeeded
:
'
scrollToNoteIfNeeded
'
,
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
.
isPollingInitialized
=
true
;
},
checkLocationHash
()
{
const
hash
=
getLocationHash
();
const
element
=
document
.
getElementById
(
hash
);
this
.
setLastFetchedAt
(
this
.
getNotesDataByProp
(
'
lastFetchedAt
'
));
if
(
hash
&&
element
)
{
this
.
setTargetNoteHash
(
hash
);
this
.
scrollToNoteIfNeeded
(
$
(
element
));
}
},
this
.
poll
();
this
.
isPollingInitialized
=
true
;
},
checkLocationHash
()
{
const
hash
=
getLocationHash
();
const
element
=
document
.
getElementById
(
hash
);
if
(
hash
&&
element
)
{
this
.
setTargetNoteHash
(
hash
);
this
.
scrollToNoteIfNeeded
(
$
(
element
));
}
},
};
},
};
</
script
>
<
template
>
...
...
app/assets/javascripts/notes/index.js
View file @
88bd9bac
import
Vue
from
'
vue
'
;
import
notesApp
from
'
./components/notes_app.vue
'
;
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Vue
({
el
:
'
#js-vue-notes
'
,
components
:
{
notesApp
,
},
data
()
{
const
notesDataset
=
document
.
getElementById
(
'
js-vue-notes
'
).
dataset
;
const
parsedUserData
=
JSON
.
parse
(
notesDataset
.
currentUserData
);
const
currentUserData
=
parsedUserData
?
{
id
:
parsedUserData
.
id
,
name
:
parsedUserData
.
name
,
username
:
parsedUserData
.
username
,
avatar_url
:
parsedUserData
.
avatar_path
||
parsedUserData
.
avatar_url
,
path
:
parsedUserData
.
path
,
}
:
{};
document
.
addEventListener
(
'
DOMContentLoaded
'
,
()
=>
new
Vue
({
el
:
'
#js-vue-notes
'
,
components
:
{
notesApp
,
},
data
()
{
const
notesDataset
=
document
.
getElementById
(
'
js-vue-notes
'
).
dataset
;
const
parsedUserData
=
JSON
.
parse
(
notesDataset
.
currentUserData
);
let
currentUserData
=
{};
if
(
parsedUserData
)
{
currentUserData
=
{
id
:
parsedUserData
.
id
,
name
:
parsedUserData
.
name
,
username
:
parsedUserData
.
username
,
avatar_url
:
parsedUserData
.
avatar_path
||
parsedUserData
.
avatar_url
,
path
:
parsedUserData
.
path
,
};
}
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
,
notesData
:
JSON
.
parse
(
notesDataset
.
notesData
),
};
},
render
(
createElement
)
{
return
createElement
(
'
notes-app
'
,
{
props
:
{
noteableData
:
this
.
noteableData
,
notesData
:
this
.
notesData
,
userData
:
this
.
currentUserData
,
return
{
noteableData
:
JSON
.
parse
(
notesDataset
.
noteableData
),
currentUserData
,
notesData
:
JSON
.
parse
(
notesDataset
.
notesData
),
};
},
render
(
createElement
)
{
return
createElement
(
'
notes-app
'
,
{
props
:
{
noteableData
:
this
.
noteableData
,
notesData
:
this
.
notesData
,
userData
:
this
.
currentUserData
,
},
});
},
});
},
}));
}),
);
app/assets/javascripts/notes/mixins/autosave.js
View file @
88bd9bac
...
...
@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export
default
{
methods
:
{
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
()
{
this
.
autosave
.
reset
();
...
...
app/assets/javascripts/notes/mixins/resolvable.js
View file @
88bd9bac
...
...
@@ -12,7 +12,8 @@ export default {
discussionResolved
()
{
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
);
}
...
...
@@ -26,7 +27,9 @@ export default {
return
__
(
'
Comment and resolve discussion
'
);
}
return
this
.
discussionResolved
?
__
(
'
Unresolve discussion
'
)
:
__
(
'
Resolve discussion
'
);
return
this
.
discussionResolved
?
__
(
'
Unresolve discussion
'
)
:
__
(
'
Resolve discussion
'
);
},
},
methods
:
{
...
...
@@ -42,7 +45,9 @@ export default {
})
.
catch
(()
=>
{
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
);
});
},
...
...
app/assets/javascripts/notes/services/notes_service.js
View file @
88bd9bac
...
...
@@ -22,7 +22,9 @@ export default {
},
toggleResolveNote
(
endpoint
,
isResolved
)
{
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
);
},
...
...
app/assets/javascripts/notes/stores/actions.js
View file @
88bd9bac
...
...
@@ -12,97 +12,115 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let
eTagPoll
;
export
const
setNotesData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_NOTES_DATA
,
data
);
export
const
setNoteableData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_NOTEABLE_DATA
,
data
);
export
const
setUserData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_USER_DATA
,
data
);
export
const
setLastFetchedAt
=
({
commit
},
data
)
=>
commit
(
types
.
SET_LAST_FETCHED_AT
,
data
);
export
const
setInitialNotes
=
({
commit
},
data
)
=>
commit
(
types
.
SET_INITIAL_NOTES
,
data
);
export
const
setTargetNoteHash
=
({
commit
},
data
)
=>
commit
(
types
.
SET_TARGET_NOTE_HASH
,
data
);
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
setNotesData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_NOTES_DATA
,
data
);
export
const
setNoteableData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_NOTEABLE_DATA
,
data
);
export
const
setUserData
=
({
commit
},
data
)
=>
commit
(
types
.
SET_USER_DATA
,
data
);
export
const
setLastFetchedAt
=
({
commit
},
data
)
=>
commit
(
types
.
SET_LAST_FETCHED_AT
,
data
);
export
const
setInitialNotes
=
({
commit
},
data
)
=>
commit
(
types
.
SET_INITIAL_NOTES
,
data
);
export
const
setTargetNoteHash
=
({
commit
},
data
)
=>
commit
(
types
.
SET_TARGET_NOTE_HASH
,
data
);
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
.
deleteNote
(
note
.
path
)
.
then
(()
=>
{
export
const
deleteNote
=
({
commit
},
note
)
=>
service
.
deleteNote
(
note
.
path
).
then
(()
=>
{
commit
(
types
.
DELETE_NOTE
,
note
);
});
export
const
updateNote
=
({
commit
},
{
endpoint
,
note
})
=>
service
.
updateNote
(
endpoint
,
note
)
.
then
(
res
=>
res
.
json
())
.
then
((
res
)
=>
{
commit
(
types
.
UPDATE_NOTE
,
res
);
});
export
const
updateNote
=
({
commit
},
{
endpoint
,
note
})
=>
service
.
updateNote
(
endpoint
,
note
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
{
commit
(
types
.
UPDATE_NOTE
,
res
);
});
export
const
replyToDiscussion
=
({
commit
},
{
endpoint
,
data
})
=>
service
.
replyToDiscussion
(
endpoint
,
data
)
.
then
(
res
=>
res
.
json
())
.
then
((
res
)
=>
{
commit
(
types
.
ADD_NEW_REPLY_TO_DISCUSSION
,
res
);
export
const
replyToDiscussion
=
({
commit
},
{
endpoint
,
data
})
=>
service
.
replyToDiscussion
(
endpoint
,
data
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
{
commit
(
types
.
ADD_NEW_REPLY_TO_DISCUSSION
,
res
);
return
res
;
});
return
res
;
});
export
const
createNewNote
=
({
commit
},
{
endpoint
,
data
})
=>
service
.
createNewNote
(
endpoint
,
data
)
.
then
(
res
=>
res
.
json
())
.
then
((
res
)
=>
{
if
(
!
res
.
errors
)
{
commit
(
types
.
ADD_NEW_NOTE
,
res
);
}
return
res
;
});
export
const
createNewNote
=
({
commit
},
{
endpoint
,
data
})
=>
service
.
createNewNote
(
endpoint
,
data
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
{
if
(
!
res
.
errors
)
{
commit
(
types
.
ADD_NEW_NOTE
,
res
);
}
return
res
;
});
export
const
removePlaceholderNotes
=
({
commit
})
=>
commit
(
types
.
REMOVE_PLACEHOLDER_NOTES
);
export
const
toggleResolveNote
=
({
commit
},
{
endpoint
,
isResolved
,
discussion
})
=>
service
.
toggleResolveNote
(
endpoint
,
isResolved
)
.
then
(
res
=>
res
.
json
())
.
then
((
res
)
=>
{
const
mutationType
=
discussion
?
types
.
UPDATE_DISCUSSION
:
types
.
UPDATE_NOTE
;
export
const
toggleResolveNote
=
(
{
commit
},
{
endpoint
,
isResolved
,
discussion
},
)
=>
service
.
toggleResolveNote
(
endpoint
,
isResolved
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
{
const
mutationType
=
discussion
?
types
.
UPDATE_DISCUSSION
:
types
.
UPDATE_NOTE
;
commit
(
mutationType
,
res
);
});
commit
(
mutationType
,
res
);
});
export
const
closeIssue
=
({
commit
,
dispatch
,
state
})
=>
{
dispatch
(
'
toggleStateButtonLoading
'
,
true
);
return
service
.
toggleIssueState
(
state
.
notesData
.
closePath
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
commit
(
types
.
CLOSE_ISSUE
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
dispatch
(
'
toggleStateButtonLoading
'
,
false
);
});
.
toggleIssueState
(
state
.
notesData
.
closePath
)
.
then
(
res
=>
res
.
json
())
.
then
(
data
=>
{
commit
(
types
.
CLOSE_ISSUE
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
dispatch
(
'
toggleStateButtonLoading
'
,
false
);
});
};
export
const
reopenIssue
=
({
commit
,
dispatch
,
state
})
=>
{
dispatch
(
'
toggleStateButtonLoading
'
,
true
);
return
service
.
toggleIssueState
(
state
.
notesData
.
reopenPath
)
.
then
(
res
=>
res
.
json
())
.
then
((
data
)
=>
{
commit
(
types
.
REOPEN_ISSUE
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
dispatch
(
'
toggleStateButtonLoading
'
,
false
);
});
.
toggleIssueState
(
state
.
notesData
.
reopenPath
)
.
then
(
res
=>
res
.
json
())
.
then
(
data
=>
{
commit
(
types
.
REOPEN_ISSUE
);
dispatch
(
'
emitStateChangedEvent
'
,
data
);
dispatch
(
'
toggleStateButtonLoading
'
,
false
);
});
};
export
const
toggleStateButtonLoading
=
({
commit
},
value
)
=>
commit
(
types
.
TOGGLE_STATE_BUTTON_LOADING
,
value
);
export
const
emitStateChangedEvent
=
({
commit
,
getters
},
data
)
=>
{
const
event
=
new
CustomEvent
(
'
issuable_vue_app:change
'
,
{
detail
:
{
data
,
isClosed
:
getters
.
openState
===
constants
.
CLOSED
,
}
});
const
event
=
new
CustomEvent
(
'
issuable_vue_app:change
'
,
{
detail
:
{
data
,
isClosed
:
getters
.
openState
===
constants
.
CLOSED
,
},
});
document
.
dispatchEvent
(
event
);
};
...
...
@@ -144,59 +162,70 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
}
return
dispatch
(
methodToDispatch
,
noteData
)
.
then
((
res
)
=>
{
const
{
errors
}
=
res
;
const
commandsChanges
=
res
.
commands_changes
;
return
dispatch
(
methodToDispatch
,
noteData
).
then
(
res
=>
{
const
{
errors
}
=
res
;
const
commandsChanges
=
res
.
commands_changes
;
if
(
hasQuickActions
&&
errors
&&
Object
.
keys
(
errors
).
length
)
{
eTagPoll
.
makeRequest
();
$
(
'
.js-gfm-input
'
).
trigger
(
'
clear-commands-cache.atwho
'
);
Flash
(
'
Commands applied
'
,
'
notice
'
,
noteData
.
flashContainer
);
}
if
(
hasQuickActions
&&
errors
&&
Object
.
keys
(
errors
).
length
)
{
eTagPoll
.
makeRequest
();
if
(
commandsChanges
)
{
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
,
);
});
}
$
(
'
.js-gfm-input
'
).
trigger
(
'
clear-commands-cache.atwho
'
);
Flash
(
'
Commands applied
'
,
'
notice
'
,
noteData
.
flashContainer
);
}
if
(
commandsChanges
.
spend_time
!=
null
||
commandsChanges
.
time_estimate
!=
null
)
{
sidebarTimeTrackingEventHub
.
$emit
(
'
timeTrackingUpdated
'
,
res
);
}
if
(
commandsChanges
)
{
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
)
{
Flash
(
errors
.
commands_only
,
'
notice
'
,
noteData
.
flashContainer
);
if
(
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
)
=>
{
if
(
resp
.
notes
&&
resp
.
notes
.
length
)
{
const
{
notesById
}
=
getters
;
resp
.
notes
.
forEach
(
(
note
)
=>
{
resp
.
notes
.
forEach
(
note
=>
{
if
(
notesById
[
note
.
id
])
{
commit
(
types
.
UPDATE_NOTE
,
note
);
}
else
if
(
note
.
type
===
constants
.
DISCUSSION_NOTE
||
note
.
type
===
constants
.
DIFF_NOTE
)
{
const
discussion
=
utils
.
findNoteObjectById
(
state
.
notes
,
note
.
discussion_id
);
}
else
if
(
note
.
type
===
constants
.
DISCUSSION_NOTE
||
note
.
type
===
constants
.
DIFF_NOTE
)
{
const
discussion
=
utils
.
findNoteObjectById
(
state
.
notes
,
note
.
discussion_id
,
);
if
(
discussion
)
{
commit
(
types
.
ADD_NEW_REPLY_TO_DISCUSSION
,
note
);
...
...
@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => {
resource
:
service
,
method
:
'
poll
'
,
data
:
state
,
successCallback
:
resp
=>
resp
.
json
()
.
then
(
data
=>
pollSuccessCallBack
(
data
,
commit
,
state
,
getters
)),
errorCallback
:
()
=>
Flash
(
'
Something went wrong while fetching latest comments.
'
),
successCallback
:
resp
=>
resp
.
json
()
.
then
(
data
=>
pollSuccessCallBack
(
data
,
commit
,
state
,
getters
)),
errorCallback
:
()
=>
Flash
(
'
Something went wrong while fetching latest comments.
'
),
});
if
(
!
Visibility
.
hidden
())
{
...
...
@@ -248,15 +280,22 @@ export const restartPolling = () => {
};
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
(
data
=>
pollSuccessCallBack
(
data
,
commit
,
state
,
getters
))
.
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
]
});
};
...
...
app/assets/javascripts/notes/stores/getters.js
View file @
88bd9bac
...
...
@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export
const
openState
=
state
=>
state
.
noteableData
.
state
;
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
)
=>
{
note
.
notes
.
every
(
n
=>
Object
.
assign
(
acc
,
{
[
n
.
id
]:
n
}));
return
acc
;
},
{});
export
const
notesById
=
state
=>
state
.
notes
.
reduce
((
acc
,
note
)
=>
{
note
.
notes
.
every
(
n
=>
Object
.
assign
(
acc
,
{
[
n
.
id
]:
n
}));
return
acc
;
},
{});
const
reverseNotes
=
array
=>
array
.
slice
(
0
).
reverse
();
const
isLastNote
=
(
note
,
state
)
=>
!
note
.
system
&&
state
.
userData
&&
note
.
author
&&
const
isLastNote
=
(
note
,
state
)
=>
!
note
.
system
&&
state
.
userData
&&
note
.
author
&&
note
.
author
.
id
===
state
.
userData
.
id
;
export
const
getCurrentUserLastNote
=
state
=>
_
.
flatten
(
reverseNotes
(
state
.
notes
)
.
map
(
note
=>
reverseNotes
(
note
.
notes
)),
export
const
getCurrentUserLastNote
=
state
=>
_
.
flatten
(
reverseNotes
(
state
.
notes
)
.
map
(
note
=>
reverseNotes
(
note
.
notes
)),
).
find
(
el
=>
isLastNote
(
el
,
state
));
export
const
getDiscussionLastNote
=
state
=>
discussion
=>
reverseNotes
(
discussion
.
notes
)
.
find
(
el
=>
isLastNote
(
el
,
state
));
export
const
getDiscussionLastNote
=
state
=>
discussion
=>
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
);
return
discussions
.
length
;
...
...
@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return
state
.
notes
.
filter
(
n
=>
!
n
.
individual_note
&&
!
resolvedMap
[
n
.
id
]);
};
export
const
resolvedDiscussionsById
=
(
state
)
=>
{
export
const
resolvedDiscussionsById
=
state
=>
{
const
map
=
{};
state
.
notes
.
forEach
(
(
n
)
=>
{
state
.
notes
.
forEach
(
n
=>
{
if
(
n
.
notes
)
{
const
resolved
=
n
.
notes
.
every
(
note
=>
note
.
resolved
&&
!
note
.
system
);
...
...
app/assets/javascripts/notes/stores/mutations.js
View file @
88bd9bac
...
...
@@ -7,7 +7,7 @@ export default {
[
types
.
ADD_NEW_NOTE
](
state
,
note
)
{
const
{
discussion_id
,
type
}
=
note
;
const
[
exists
]
=
state
.
notes
.
filter
(
n
=>
n
.
id
===
note
.
discussion_id
);
const
isDiscussion
=
(
type
===
constants
.
DISCUSSION_NOTE
)
;
const
isDiscussion
=
type
===
constants
.
DISCUSSION_NOTE
;
if
(
!
exists
)
{
const
noteData
=
{
...
...
@@ -63,13 +63,15 @@ export default {
const
note
=
notes
[
i
];
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
)
{
if
(
children
[
j
].
isPlaceholderNote
)
{
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
);
}
}
...
...
@@ -89,10 +91,10 @@ export default {
[
types
.
SET_INITIAL_NOTES
](
state
,
notesData
)
{
const
notes
=
[];
notesData
.
forEach
(
(
note
)
=>
{
notesData
.
forEach
(
note
=>
{
// To support legacy notes, should be very rare case.
if
(
note
.
individual_note
&&
note
.
notes
.
length
>
1
)
{
note
.
notes
.
forEach
(
(
n
)
=>
{
note
.
notes
.
forEach
(
n
=>
{
notes
.
push
({
...
note
,
notes
:
[
n
],
// override notes array to only have one item to mimick individual_note
...
...
@@ -103,7 +105,7 @@ export default {
notes
.
push
({
...
note
,
expanded
:
(
oldNote
?
oldNote
.
expanded
:
note
.
expanded
)
,
expanded
:
oldNote
?
oldNote
.
expanded
:
note
.
expanded
,
});
}
});
...
...
@@ -128,7 +130,9 @@ export default {
notesArr
.
push
({
individual_note
:
true
,
isPlaceholderNote
:
true
,
placeholderType
:
data
.
isSystemNote
?
constants
.
SYSTEM_NOTE
:
constants
.
NOTE
,
placeholderType
:
data
.
isSystemNote
?
constants
.
SYSTEM_NOTE
:
constants
.
NOTE
,
notes
:
[
{
body
:
data
.
noteBody
,
...
...
@@ -141,12 +145,16 @@ export default {
const
{
awardName
,
note
}
=
data
;
const
{
id
,
name
,
username
}
=
state
.
userData
;
const
hasEmojiAwardedByCurrentUser
=
note
.
award_emoji
.
filter
(
emoji
=>
emoji
.
name
===
data
.
awardName
&&
emoji
.
user
.
id
===
id
);
const
hasEmojiAwardedByCurrentUser
=
note
.
award_emoji
.
filter
(
emoji
=>
emoji
.
name
===
data
.
awardName
&&
emoji
.
user
.
id
===
id
,
);
if
(
hasEmojiAwardedByCurrentUser
.
length
)
{
// 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
{
note
.
award_emoji
.
push
({
name
:
awardName
,
...
...
app/assets/javascripts/notes/stores/utils.js
View file @
88bd9bac
...
...
@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
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
'
;
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
}
`
);
return
commandRegex
.
test
(
note
);
});
...
...
@@ -27,4 +29,5 @@ export const getQuickActionText = (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