Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
ffef1669
Commit
ffef1669
authored
Jul 26, 2017
by
Filipa Lacerda
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Use mapActions, mapGetters and mapMutations for components
parent
4e81ad2a
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
850 additions
and
804 deletions
+850
-804
app/assets/javascripts/notes/components/issue_comment_form.vue
...ssets/javascripts/notes/components/issue_comment_form.vue
+5
-1
app/assets/javascripts/notes/components/issue_discussion.vue
app/assets/javascripts/notes/components/issue_discussion.vue
+100
-96
app/assets/javascripts/notes/components/issue_note.vue
app/assets/javascripts/notes/components/issue_note.vue
+107
-101
app/assets/javascripts/notes/components/issue_note_actions.vue
...ssets/javascripts/notes/components/issue_note_actions.vue
+70
-63
app/assets/javascripts/notes/components/issue_note_awards_list.vue
...s/javascripts/notes/components/issue_note_awards_list.vue
+160
-155
app/assets/javascripts/notes/components/issue_note_body.vue
app/assets/javascripts/notes/components/issue_note_body.vue
+59
-59
app/assets/javascripts/notes/components/issue_note_edited_text.vue
...s/javascripts/notes/components/issue_note_edited_text.vue
+25
-24
app/assets/javascripts/notes/components/issue_note_form.vue
app/assets/javascripts/notes/components/issue_note_form.vue
+74
-74
app/assets/javascripts/notes/components/issue_note_header.vue
...assets/javascripts/notes/components/issue_note_header.vue
+67
-59
app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue
...scripts/notes/components/issue_note_signed_out_widget.vue
+13
-13
app/assets/javascripts/notes/components/issue_notes.vue
app/assets/javascripts/notes/components/issue_notes.vue
+118
-107
app/assets/javascripts/notes/components/issue_placeholder_note.vue
...s/javascripts/notes/components/issue_placeholder_note.vue
+14
-13
app/assets/javascripts/notes/components/issue_placeholder_system_note.vue
...cripts/notes/components/issue_placeholder_system_note.vue
+7
-7
app/assets/javascripts/notes/components/issue_system_note.vue
...assets/javascripts/notes/components/issue_system_note.vue
+27
-27
app/assets/javascripts/notes/constants.js
app/assets/javascripts/notes/constants.js
+1
-1
app/assets/javascripts/notes/stores/actions.js
app/assets/javascripts/notes/stores/actions.js
+3
-4
No files found.
app/assets/javascripts/notes/components/issue_comment_form.vue
View file @
ffef1669
<
script
>
<
script
>
/* global Flash */
/* global Flash */
import
{
mapActions
}
from
'
vuex
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
issueNoteSignedOutWidget
from
'
./issue_note_signed_out_widget.vue
'
;
import
issueNoteSignedOutWidget
from
'
./issue_note_signed_out_widget.vue
'
;
...
@@ -60,6 +61,9 @@
...
@@ -60,6 +61,9 @@
},
},
},
},
methods
:
{
methods
:
{
...
mapActions
([
'
saveNote
'
]),
handleSave
(
withIssueAction
)
{
handleSave
(
withIssueAction
)
{
if
(
this
.
note
.
length
)
{
if
(
this
.
note
.
length
)
{
const
noteData
=
{
const
noteData
=
{
...
@@ -79,7 +83,7 @@
...
@@ -79,7 +83,7 @@
noteData
.
data
.
note
.
type
=
constants
.
DISCUSSION_NOTE
;
noteData
.
data
.
note
.
type
=
constants
.
DISCUSSION_NOTE
;
}
}
this
.
$store
.
dispatch
(
'
saveNote
'
,
noteData
)
this
.
saveNote
(
noteData
)
.
then
((
res
)
=>
{
.
then
((
res
)
=>
{
if
(
res
.
errors
)
{
if
(
res
.
errors
)
{
if
(
res
.
errors
.
commands_only
)
{
if
(
res
.
errors
.
commands_only
)
{
...
...
app/assets/javascripts/notes/components/issue_discussion.vue
View file @
ffef1669
<
script
>
<
script
>
/* global Flash */
/* global Flash */
import
{
mapActions
}
from
'
vuex
'
;
import
{
TOGGLE_DISCUSSION
}
from
'
../stores/mutation_types
'
;
import
{
SYSTEM_NOTE
}
from
'
../constants
'
;
import
issueNote
from
'
./issue_note.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
issueNoteHeader
from
'
./issue_note_header.vue
'
;
import
issueNoteActions
from
'
./issue_note_actions.vue
'
;
import
issueNoteSignedOutWidget
from
'
./issue_note_signed_out_widget.vue
'
;
import
issueNoteEditedText
from
'
./issue_note_edited_text.vue
'
;
import
issueNoteForm
from
'
./issue_note_form.vue
'
;
import
placeholderNote
from
'
./issue_placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
./issue_placeholder_system_note.vue
'
;
import
issueNote
from
'
./issue_note.vue
'
;
export
default
{
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
props
:
{
import
issueNoteHeader
from
'
./issue_note_header.vue
'
;
note
:
{
import
issueNoteActions
from
'
./issue_note_actions.vue
'
;
type
:
Object
,
import
issueNoteSignedOutWidget
from
'
./issue_note_signed_out_widget.vue
'
;
required
:
true
,
import
issueNoteEditedText
from
'
./issue_note_edited_text.vue
'
;
},
import
issueNoteForm
from
'
./issue_note_form.vue
'
;
import
placeholderNote
from
'
./issue_placeholder_note.vue
'
;
import
placeholderSystemNote
from
'
./issue_placeholder_system_note.vue
'
;
export
default
{
props
:
{
note
:
{
type
:
Object
,
required
:
true
,
},
},
},
data
()
{
data
()
{
return
{
return
{
newNotePath
:
window
.
gl
.
issueData
.
create_note_path
,
newNotePath
:
window
.
gl
.
issueData
.
create_note_path
,
isReplying
:
false
,
isReplying
:
false
,
};
};
},
components
:
{
issueNote
,
userAvatarLink
,
issueNoteHeader
,
issueNoteActions
,
issueNoteSignedOutWidget
,
issueNoteEditedText
,
issueNoteForm
,
placeholderNote
,
placeholderSystemNote
,
},
computed
:
{
discussion
()
{
return
this
.
note
.
notes
[
0
];
},
},
author
()
{
components
:
{
return
this
.
discussion
.
author
;
issueNote
,
userAvatarLink
,
issueNoteHeader
,
issueNoteActions
,
issueNoteSignedOutWidget
,
issueNoteEditedText
,
issueNoteForm
,
placeholderNote
,
placeholderSystemNote
,
},
},
canReply
()
{
computed
:
{
return
window
.
gl
.
issueData
.
current_user
.
can_create_note
;
discussion
()
{
return
this
.
note
.
notes
[
0
];
},
author
()
{
return
this
.
discussion
.
author
;
},
canReply
()
{
return
window
.
gl
.
issueData
.
current_user
.
can_create_note
;
},
},
},
},
methods
:
{
methods
:
{
...
mapActions
([
componentName
(
note
)
{
'
saveNote
'
,
if
(
note
.
isPlaceholderNote
)
{
]),
if
(
note
.
placeholderType
===
'
systemNote
'
)
{
...
mapMutations
({
return
placeholderSystemNote
;
toggleDiscussion
:
TOGGLE_DISCUSSION
,
}),
componentName
(
note
)
{
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
}
return
placeholderNote
;
}
}
return
placeholderNote
;
}
return
issueNote
;
return
issueNote
;
},
},
componentData
(
note
)
{
componentData
(
note
)
{
return
note
.
isPlaceholderNote
?
note
.
notes
[
0
]
:
note
;
return
note
.
isPlaceholderNote
?
note
.
notes
[
0
]
:
note
;
},
},
toggleDiscussion
()
{
toggleDiscussion
()
{
this
.
$store
.
commit
(
'
toggleDiscussion
'
,
{
this
.
toggleDiscussion
({
discussionId
:
this
.
note
.
id
});
discussionId
:
this
.
note
.
id
,
},
});
showReplyForm
()
{
},
this
.
isReplying
=
true
;
showReplyForm
()
{
},
this
.
isReplying
=
true
;
cancelReplyForm
(
shouldConfirm
)
{
},
if
(
shouldConfirm
&&
this
.
$refs
.
noteForm
.
isDirty
)
{
cancelReplyForm
(
shouldConfirm
)
{
const
msg
=
'
Are you sure you want to cancel creating this comment?
'
;
if
(
shouldConfirm
&&
this
.
$refs
.
noteForm
.
isDirty
)
{
// eslint-disable-next-line no-alert
const
msg
=
'
Are you sure you want to cancel creating this comment?
'
;
const
isConfirmed
=
confirm
(
msg
);
// eslint-disable-next-line no-alert
if
(
!
isConfirmed
)
{
const
isConfirmed
=
confirm
(
msg
);
return
;
if
(
!
isConfirmed
)
{
}
return
;
}
}
}
this
.
isReplying
=
false
;
this
.
isReplying
=
false
;
},
},
saveReply
({
note
})
{
saveReply
({
note
})
{
const
replyData
=
{
const
replyData
=
{
endpoint
:
this
.
newNotePath
,
endpoint
:
this
.
newNotePath
,
flashContainer
:
this
.
$el
,
flashContainer
:
this
.
$el
,
data
:
{
data
:
{
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
in_reply_to_discussion_id
:
this
.
note
.
reply_id
,
target_type
:
'
issue
'
,
target_type
:
'
issue
'
,
target_id
:
this
.
discussion
.
noteable_id
,
target_id
:
this
.
discussion
.
noteable_id
,
note
:
{
note
},
note
:
{
note
},
full_data
:
true
,
full_data
:
true
,
},
},
};
};
this
.
$store
.
dispatch
(
'
saveNote
'
,
replyData
)
this
.
saveNote
(
replyData
)
.
then
(()
=>
{
.
then
(()
=>
{
this
.
isReplying
=
false
;
this
.
isReplying
=
false
;
})
})
.
catch
(()
=>
{
.
catch
(()
=>
Flash
(
'
Something went wrong while adding your reply. Please try again.
'
));
Flash
(
'
Something went wrong while adding your reply. Please try again.
'
);
},
});
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -132,8 +136,7 @@ export default {
...
@@ -132,8 +136,7 @@ export default {
:edited-at=
"note.last_updated_at"
:edited-at=
"note.last_updated_at"
:edited-by=
"note.last_updated_by"
:edited-by=
"note.last_updated_by"
actionText=
"Last updated"
actionText=
"Last updated"
className=
"discussion-headline-light js-discussion-headline"
className=
"discussion-headline-light js-discussion-headline"
/>
/>
</div>
</div>
</div>
</div>
<div
<div
...
@@ -162,7 +165,8 @@ export default {
...
@@ -162,7 +165,8 @@ export default {
saveButtonTitle=
"Comment"
saveButtonTitle=
"Comment"
:update-handler=
"saveReply"
:update-handler=
"saveReply"
:cancel-handler=
"cancelReplyForm"
:cancel-handler=
"cancelReplyForm"
ref=
"noteForm"
/>
ref=
"noteForm"
/>
<issue-note-signed-out-widget
v-if=
"!canReply"
/>
<issue-note-signed-out-widget
v-if=
"!canReply"
/>
</div>
</div>
</div>
</div>
...
...
app/assets/javascripts/notes/components/issue_note.vue
View file @
ffef1669
<
script
>
<
script
>
/* global Flash */
/* global Flash */
import
{
mapGetter
s
}
from
'
vuex
'
;
import
{
mapGetters
,
mapAction
s
}
from
'
vuex
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
userAvatarLink
from
'
../../vue_shared/components/user_avatar/user_avatar_link.vue
'
;
import
issueNoteHeader
from
'
./issue_note_header.vue
'
;
import
issueNoteHeader
from
'
./issue_note_header.vue
'
;
import
issueNoteActions
from
'
./issue_note_actions.vue
'
;
import
issueNoteActions
from
'
./issue_note_actions.vue
'
;
import
issueNoteBody
from
'
./issue_note_body.vue
'
;
import
issueNoteBody
from
'
./issue_note_body.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
export
default
{
export
default
{
props
:
{
props
:
{
note
:
{
note
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
},
},
data
()
{
data
()
{
return
{
isEditing
:
false
,
isDeleting
:
false
,
};
},
components
:
{
userAvatarLink
,
issueNoteHeader
,
issueNoteActions
,
issueNoteBody
,
},
computed
:
{
...
mapGetters
([
'
targetNoteHash
'
,
]),
author
()
{
return
this
.
note
.
author
;
},
classNameBindings
()
{
return
{
return
{
'
is-editing
'
:
this
.
isEditing
,
isEditing
:
false
,
'
disabled-content
'
:
this
.
isDeleting
,
isDeleting
:
false
,
'
js-my-note
'
:
this
.
author
.
id
===
window
.
gon
.
current_user_id
,
target
:
this
.
targetNoteHash
===
this
.
noteAnchorId
,
};
};
},
},
c
anReportAsAbuse
()
{
c
omponents
:
{
return
this
.
note
.
report_abuse_path
&&
this
.
author
.
id
!==
window
.
gon
.
current_user_id
;
userAvatarLink
,
}
,
issueNoteHeader
,
noteAnchorId
()
{
issueNoteActions
,
return
`note_
${
this
.
note
.
id
}
`
;
issueNoteBody
,
},
},
},
computed
:
{
methods
:
{
...
mapGetters
([
editHandler
()
{
'
targetNoteHash
'
,
this
.
isEditing
=
true
;
]),
author
()
{
return
this
.
note
.
author
;
},
classNameBindings
()
{
return
{
'
is-editing
'
:
this
.
isEditing
,
'
disabled-content
'
:
this
.
isDeleting
,
'
js-my-note
'
:
this
.
author
.
id
===
window
.
gon
.
current_user_id
,
target
:
this
.
targetNoteHash
===
this
.
noteAnchorId
,
};
},
canReportAsAbuse
()
{
return
this
.
note
.
report_abuse_path
&&
this
.
author
.
id
!==
window
.
gon
.
current_user_id
;
},
noteAnchorId
()
{
return
`note_
${
this
.
note
.
id
}
`
;
},
},
},
deleteHandler
()
{
methods
:
{
const
msg
=
'
Are you sure you want to delete this list?
'
;
...
mapActions
([
const
isConfirmed
=
confirm
(
msg
);
// eslint-disable-line
'
deleteNote
'
,
'
updateNote
'
,
'
scrollToNoteIfNeeded
'
,
]),
editHandler
()
{
this
.
isEditing
=
true
;
},
deleteHandler
()
{
const
msg
=
'
Are you sure you want to delete this list?
'
;
const
isConfirmed
=
confirm
(
msg
);
// eslint-disable-line
if
(
isConfirmed
)
{
this
.
isDeleting
=
true
;
this
.
deleteNote
(
this
.
note
)
.
then
(()
=>
{
this
.
isDeleting
=
false
;
})
.
catch
(()
=>
{
Flash
(
'
Something went wrong while deleting your note. Please try again.
'
);
this
.
isDeleting
=
false
;
});
}
},
formUpdateHandler
(
note
)
{
const
data
=
{
endpoint
:
this
.
note
.
path
,
note
:
{
full_data
:
true
,
target_type
:
'
issue
'
,
target_id
:
this
.
note
.
noteable_id
,
note
,
},
};
if
(
isConfirmed
)
{
this
.
updateNote
(
data
)
this
.
isDeleting
=
true
;
this
.
$store
.
dispatch
(
'
deleteNote
'
,
this
.
note
)
.
then
(()
=>
{
.
then
(()
=>
{
this
.
isDeleting
=
false
;
this
.
isEditing
=
false
;
$
(
this
.
$refs
.
noteBody
.
$el
).
renderGFM
();
})
})
.
catch
(()
=>
{
.
catch
(()
=>
Flash
(
'
Something went wrong while editing your comment. Please try again.
'
));
new
Flash
(
'
Something went wrong while deleting your note. Please try again.
'
);
// eslint-disable-line
},
this
.
isDeleting
=
false
;
formCancelHandler
(
shouldConfirm
)
{
});
if
(
shouldConfirm
&&
this
.
$refs
.
noteBody
.
$refs
.
noteForm
.
isDirty
)
{
}
const
msg
=
'
Are you sure you want to cancel editing this comment?
'
;
},
const
isConfirmed
=
confirm
(
msg
);
// eslint-disable-line
formUpdateHandler
(
note
)
{
if
(
!
isConfirmed
)
{
const
data
=
{
return
;
endpoint
:
this
.
note
.
path
,
}
note
:
{
}
full_data
:
true
,
target_type
:
'
issue
'
,
target_id
:
this
.
note
.
noteable_id
,
note
,
},
};
this
.
$store
.
dispatch
(
'
updateNote
'
,
data
)
this
.
isEditing
=
false
;
.
then
(()
=>
{
},
this
.
isEditing
=
false
;
$
(
this
.
$refs
.
noteBody
.
$el
).
renderGFM
();
})
.
catch
(()
=>
{
Flash
(
'
Something went wrong while editing your comment. Please try again.
'
);
});
},
},
formCancelHandler
(
shouldConfirm
)
{
created
()
{
if
(
shouldConfirm
&&
this
.
$refs
.
noteBody
.
$refs
.
noteForm
.
isDirty
)
{
eventHub
.
$on
(
'
enterEditMode
'
,
({
noteId
})
=>
{
const
msg
=
'
Are you sure you want to cancel editing this comment?
'
;
if
(
noteId
===
this
.
note
.
id
)
{
const
isConfirmed
=
confirm
(
msg
);
// eslint-disable-line
this
.
isEditing
=
true
;
if
(
!
isConfirmed
)
{
this
.
scrollToNoteIfNeeded
(
$
(
this
.
$el
));
return
;
}
}
}
});
this
.
isEditing
=
false
;
},
},
},
};
created
()
{
eventHub
.
$on
(
'
enterEditMode
'
,
({
noteId
})
=>
{
if
(
noteId
===
this
.
note
.
id
)
{
this
.
isEditing
=
true
;
this
.
$store
.
dispatch
(
'
scrollToNoteIfNeeded
'
,
$
(
this
.
$el
));
}
});
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -124,7 +126,8 @@ export default {
...
@@ -124,7 +126,8 @@ export default {
:link-href=
"author.path"
:link-href=
"author.path"
:img-src=
"author.avatar_url"
:img-src=
"author.avatar_url"
:img-alt=
"author.name"
:img-alt=
"author.name"
:img-size=
"40"
/>
:img-size=
"40"
/>
</div>
</div>
<div
class=
"timeline-content"
>
<div
class=
"timeline-content"
>
<div
class=
"note-header"
>
<div
class=
"note-header"
>
...
@@ -132,7 +135,8 @@ export default {
...
@@ -132,7 +135,8 @@ export default {
:author=
"author"
:author=
"author"
:created-at=
"note.created_at"
:created-at=
"note.created_at"
:note-id=
"note.id"
:note-id=
"note.id"
actionText=
"commented"
/>
actionText=
"commented"
/>
<issue-note-actions
<issue-note-actions
:author-id=
"author.id"
:author-id=
"author.id"
:note-id=
"note.id"
:note-id=
"note.id"
...
@@ -142,7 +146,8 @@ export default {
...
@@ -142,7 +146,8 @@ export default {
:can-report-as-abuse=
"canReportAsAbuse"
:can-report-as-abuse=
"canReportAsAbuse"
:report-abuse-path=
"note.report_abuse_path"
:report-abuse-path=
"note.report_abuse_path"
:edit-handler=
"editHandler"
:edit-handler=
"editHandler"
:delete-handler=
"deleteHandler"
/>
:delete-handler=
"deleteHandler"
/>
</div>
</div>
<issue-note-body
<issue-note-body
:note=
"note"
:note=
"note"
...
@@ -150,7 +155,8 @@ export default {
...
@@ -150,7 +155,8 @@ export default {
:is-editing=
"isEditing"
:is-editing=
"isEditing"
:form-update-handler=
"formUpdateHandler"
:form-update-handler=
"formUpdateHandler"
:form-cancel-handler=
"formCancelHandler"
:form-cancel-handler=
"formCancelHandler"
ref=
"noteBody"
/>
ref=
"noteBody"
/>
</div>
</div>
</div>
</div>
</li>
</li>
...
...
app/assets/javascripts/notes/components/issue_note_actions.vue
View file @
ffef1669
<
script
>
<
script
>
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
loadingIcon
from
'
../../vue_shared/components/loadingI
con.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_i
con.vue
'
;
export
default
{
export
default
{
props
:
{
props
:
{
authorId
:
{
authorId
:
{
type
:
Number
,
type
:
Number
,
required
:
true
,
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
,
},
canReportAsAbuse
:
{
type
:
Boolean
,
required
:
true
,
},
editHandler
:
{
type
:
Function
,
required
:
true
,
},
deleteHandler
:
{
type
:
Function
,
required
:
true
,
},
},
},
noteId
:
{
data
()
{
type
:
Number
,
return
{
required
:
true
,
emojiSmiling
,
emojiSmile
,
emojiSmiley
,
};
},
},
accessLevel
:
{
components
:
{
type
:
String
,
loadingIcon
,
required
:
false
,
default
:
''
,
},
},
reportAbusePath
:
{
computed
:
{
type
:
String
,
shouldShowActionsDropdown
()
{
required
:
true
,
return
window
.
gon
.
current_user_id
&&
(
this
.
canEdit
||
this
.
canReportAsAbuse
);
},
canAddAwardEmoji
()
{
return
window
.
gon
.
current_user_id
;
},
isAuthoredByMe
()
{
return
this
.
authorId
===
window
.
gon
.
current_user_id
;
},
},
},
canEdit
:
{
};
type
:
Boolean
,
required
:
true
,
},
canDelete
:
{
type
:
Boolean
,
required
:
true
,
},
canReportAsAbuse
:
{
type
:
Boolean
,
required
:
true
,
},
editHandler
:
{
type
:
Function
,
required
:
true
,
},
deleteHandler
:
{
type
:
Function
,
required
:
true
,
},
},
data
()
{
return
{
emojiSmiling
,
emojiSmile
,
emojiSmiley
,
};
},
computed
:
{
shouldShowActionsDropdown
()
{
return
window
.
gon
.
current_user_id
&&
(
this
.
canEdit
||
this
.
canReportAsAbuse
);
},
canAddAwardEmoji
()
{
return
window
.
gon
.
current_user_id
;
},
isAuthoredByMe
()
{
return
this
.
authorId
===
window
.
gon
.
current_user_id
;
},
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -82,13 +85,16 @@ export default {
...
@@ -82,13 +85,16 @@ export default {
<loading-icon
/>
<loading-icon
/>
<span
<span
v-html=
"emojiSmiling"
v-html=
"emojiSmiling"
class=
"link-highlight award-control-icon-neutral"
></span>
class=
"link-highlight award-control-icon-neutral"
>
</span>
<span
<span
v-html=
"emojiSmiley"
v-html=
"emojiSmiley"
class=
"link-highlight award-control-icon-positive"
></span>
class=
"link-highlight award-control-icon-positive"
>
</span>
<span
<span
v-html=
"emojiSmile"
v-html=
"emojiSmile"
class=
"link-highlight award-control-icon-super-positive"
></span>
class=
"link-highlight award-control-icon-super-positive"
>
</span>
</a>
</a>
<div
<div
v-if=
"shouldShowActionsDropdown"
v-if=
"shouldShowActionsDropdown"
...
@@ -101,7 +107,8 @@ export default {
...
@@ -101,7 +107,8 @@ export default {
data-container=
"body"
>
data-container=
"body"
>
<i
<i
aria-hidden=
"true"
aria-hidden=
"true"
class=
"fa fa-ellipsis-v icon"
></i>
class=
"fa fa-ellipsis-v icon"
>
</i>
</button>
</button>
<ul
class=
"dropdown-menu more-actions-dropdown dropdown-open-left"
>
<ul
class=
"dropdown-menu more-actions-dropdown dropdown-open-left"
>
<template
v-if=
"canEdit"
>
<template
v-if=
"canEdit"
>
...
...
app/assets/javascripts/notes/components/issue_note_awards_list.vue
View file @
ffef1669
<
script
>
<
script
>
/* global Flash */
/* global Flash */
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
{
mapActions
}
from
'
vuex
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
emojiSmiling
from
'
icons/_emoji_slightly_smiling_face.svg
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
emojiSmile
from
'
icons/_emoji_smile.svg
'
;
import
*
as
Emoji
from
'
../../emoji
'
;
import
emojiSmiley
from
'
icons/_emoji_smiley.svg
'
;
import
*
as
Emoji
from
'
../../emoji
'
;
export
default
{
props
:
{
export
default
{
awards
:
{
props
:
{
type
:
Array
,
awards
:
{
required
:
true
,
type
:
Array
,
required
:
true
,
},
toggleAwardPath
:
{
type
:
String
,
required
:
true
,
},
noteAuthorId
:
{
type
:
Number
,
required
:
true
,
},
noteId
:
{
type
:
Number
,
required
:
true
,
},
},
},
toggleAwardPath
:
{
data
()
{
type
:
String
,
const
userId
=
window
.
gon
.
current_user_id
;
required
:
true
,
},
noteAuthorId
:
{
type
:
Number
,
required
:
true
,
},
noteId
:
{
type
:
Number
,
required
:
true
,
},
},
data
()
{
const
userId
=
window
.
gon
.
current_user_id
;
return
{
emojiSmiling
,
emojiSmile
,
emojiSmiley
,
canAward
:
!!
userId
,
myUserId
:
userId
,
};
},
computed
:
{
// `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
=
{};
const
orderedAwards
=
{};
this
.
awards
.
forEach
((
award
)
=>
{
awards
[
award
.
name
]
=
awards
[
award
.
name
]
||
[];
awards
[
award
.
name
].
push
(
award
);
});
// Always show thumbsup and thumbsdown first
const
{
thumbsup
,
thumbsdown
}
=
awards
;
if
(
thumbsup
)
{
orderedAwards
.
thumbsup
=
thumbsup
;
delete
awards
.
thumbsup
;
}
if
(
thumbsdown
)
{
orderedAwards
.
thumbsdown
=
thumbsdown
;
delete
awards
.
thumbsdown
;
}
// Because for-in forbidden
const
keys
=
Object
.
keys
(
awards
);
keys
.
forEach
((
key
)
=>
{
orderedAwards
[
key
]
=
awards
[
key
];
});
return
orderedAwards
;
},
isAuthoredByMe
()
{
return
this
.
noteAuthorId
===
window
.
gon
.
current_user_id
;
},
},
methods
:
{
getAwardHTML
(
name
)
{
return
Emoji
.
glEmojiTag
(
name
);
},
getAwardClassBindings
(
awardList
,
awardName
)
{
return
{
return
{
active
:
this
.
amIAwarded
(
awardList
),
emojiSmiling
,
disabled
:
!
this
.
canInteractWithEmoji
(
awardList
,
awardName
),
emojiSmile
,
emojiSmiley
,
canAward
:
!!
userId
,
myUserId
:
userId
,
};
};
},
},
canInteractWithEmoji
(
awardList
,
awardName
)
{
computed
:
{
let
isAllowed
=
true
;
// `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
const
restrictedEmojis
=
[
'
thumbsup
'
,
'
thumbsdown
'
];
// [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
const
{
myUserId
,
noteAuthorId
}
=
this
;
// This method will group emojis by their name as an Object. See below.
// {
// Users can not add :+1: and :-1: to their notes
// foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ],
if
(
myUserId
===
noteAuthorId
&&
restrictedEmojis
.
indexOf
(
awardName
)
>
-
1
)
{
// bar: [ { name: bar, user: user1 } ]
isAllowed
=
false
;
// }
}
// We need to do this otherwise we will render the same emoji over and over again.
groupedAwards
()
{
const
awards
=
{};
const
orderedAwards
=
{};
this
.
awards
.
forEach
((
award
)
=>
{
awards
[
award
.
name
]
=
awards
[
award
.
name
]
||
[];
awards
[
award
.
name
].
push
(
award
);
});
return
this
.
canAward
&&
isAllowed
;
// Always show thumbsup and thumbsdown first
},
const
{
thumbsup
,
thumbsdown
}
=
awards
;
amIAwarded
(
awardList
)
{
if
(
thumbsup
)
{
const
isAwarded
=
awardList
.
filter
(
award
=>
award
.
user
.
id
===
this
.
myUserId
);
orderedAwards
.
thumbsup
=
thumbsup
;
delete
awards
.
thumbsup
;
}
if
(
thumbsdown
)
{
orderedAwards
.
thumbsdown
=
thumbsdown
;
delete
awards
.
thumbsdown
;
}
// Because for-in forbidden
const
keys
=
Object
.
keys
(
awards
);
keys
.
forEach
((
key
)
=>
{
orderedAwards
[
key
]
=
awards
[
key
];
});
return
isAwarded
.
length
;
return
orderedAwards
;
},
},
awardTitle
(
awardsList
)
{
isAuthoredByMe
()
{
const
amIAwarded
=
this
.
amIAwarded
(
awardsList
);
return
this
.
noteAuthorId
===
window
.
gon
.
current_user_id
;
const
TOOLTIP_NAME_COUNT
=
amIAwarded
?
9
:
10
;
},
let
awardList
=
awardsList
;
// Filter myself from list if I am awarded.
if
(
amIAwarded
)
{
awardList
=
awardList
.
filter
(
award
=>
award
.
user
.
id
!==
this
.
myUserId
);
}
// 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
(
amIAwarded
)
{
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
)
{
methods
:
{
const
data
=
{
...
mapActions
([
endpoint
:
this
.
toggleAwardPath
,
'
toggleAward
'
,
noteId
:
this
.
noteId
,
]),
awardName
,
getAwardHTML
(
name
)
{
};
return
Emoji
.
glEmojiTag
(
name
);
},
this
.
$store
.
dispatch
(
'
toggleAward
'
,
data
)
getAwardClassBindings
(
awardList
,
awardName
)
{
.
then
(()
=>
{
return
{
$
(
this
.
$el
).
find
(
'
.award-control
'
).
tooltip
(
'
fixTitle
'
);
active
:
this
.
amIAwarded
(
awardList
),
})
disabled
:
!
this
.
canInteractWithEmoji
(
awardList
,
awardName
),
.
catch
(()
=>
{
};
Flash
(
'
Something went wrong on our end.
'
);
},
});
canInteractWithEmoji
(
awardList
,
awardName
)
{
let
isAllowed
=
true
;
const
restrictedEmojis
=
[
'
thumbsup
'
,
'
thumbsdown
'
];
const
{
myUserId
,
noteAuthorId
}
=
this
;
// Users can not add :+1: and :-1: to their own notes
if
(
myUserId
===
noteAuthorId
&&
restrictedEmojis
.
indexOf
(
awardName
)
>
-
1
)
{
isAllowed
=
false
;
}
return
this
.
canAward
&&
isAllowed
;
},
amIAwarded
(
awardList
)
{
const
isAwarded
=
awardList
.
filter
(
award
=>
award
.
user
.
id
===
this
.
myUserId
);
return
isAwarded
.
length
;
},
awardTitle
(
awardsList
)
{
const
amIAwarded
=
this
.
amIAwarded
(
awardsList
);
const
TOOLTIP_NAME_COUNT
=
amIAwarded
?
9
:
10
;
let
awardList
=
awardsList
;
// Filter myself from list if I am awarded.
if
(
amIAwarded
)
{
awardList
=
awardList
.
filter
(
award
=>
award
.
user
.
id
!==
this
.
myUserId
);
}
// 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
(
amIAwarded
)
{
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
)
{
const
data
=
{
endpoint
:
this
.
toggleAwardPath
,
noteId
:
this
.
noteId
,
awardName
,
};
this
.
toggleAward
(
data
)
.
then
(()
=>
{
$
(
this
.
$el
).
find
(
'
.award-control
'
).
tooltip
(
'
fixTitle
'
);
})
.
catch
(()
=>
Flash
(
'
Something went wrong on our end.
'
));
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -189,13 +191,16 @@ export default {
...
@@ -189,13 +191,16 @@ export default {
type="button">
type="button">
<span
<span
v-html=
"emojiSmiling"
v-html=
"emojiSmiling"
class=
"award-control-icon award-control-icon-neutral"
></span>
class=
"award-control-icon award-control-icon-neutral"
>
</span>
<span
<span
v-html=
"emojiSmiley"
v-html=
"emojiSmiley"
class=
"award-control-icon award-control-icon-positive"
></span>
class=
"award-control-icon award-control-icon-positive"
>
</span>
<span
<span
v-html=
"emojiSmile"
v-html=
"emojiSmile"
class=
"award-control-icon award-control-icon-super-positive"
></span>
class=
"award-control-icon award-control-icon-super-positive"
>
</span>
<i
<i
aria-hidden=
"true"
aria-hidden=
"true"
class=
"fa fa-spinner fa-spin award-control-icon award-control-icon-loading"
></i>
class=
"fa fa-spinner fa-spin award-control-icon award-control-icon-loading"
></i>
...
...
app/assets/javascripts/notes/components/issue_note_body.vue
View file @
ffef1669
<
script
>
<
script
>
import
issueNoteEditedText
from
'
./issue_note_edited_text.vue
'
;
import
issueNoteEditedText
from
'
./issue_note_edited_text.vue
'
;
import
issueNoteAwardsList
from
'
./issue_note_awards_list.vue
'
;
import
issueNoteAwardsList
from
'
./issue_note_awards_list.vue
'
;
import
issueNoteForm
from
'
./issue_note_form.vue
'
;
import
issueNoteForm
from
'
./issue_note_form.vue
'
;
import
TaskList
from
'
../../task_list
'
;
import
TaskList
from
'
../../task_list
'
;
export
default
{
export
default
{
props
:
{
props
:
{
note
:
{
note
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
canEdit
:
{
type
:
Boolean
,
required
:
true
,
},
isEditing
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
formUpdateHandler
:
{
type
:
Function
,
required
:
true
,
},
formCancelHandler
:
{
type
:
Function
,
required
:
true
,
},
},
},
canEdit
:
{
components
:
{
type
:
Boolean
,
issueNoteEditedText
,
required
:
true
,
issueNoteAwardsList
,
issueNoteForm
,
},
},
isEditing
:
{
computed
:
{
type
:
Boolean
,
noteBody
()
{
required
:
false
,
return
this
.
note
.
note
;
default
:
false
,
}
,
},
},
formUpdateHandler
:
{
methods
:
{
type
:
Function
,
renderGFM
()
{
required
:
true
,
$
(
this
.
$refs
[
'
note-body
'
]).
renderGFM
();
},
},
formCancelHandler
:
{
initTaskList
()
{
type
:
Function
,
if
(
this
.
canEdit
)
{
required
:
true
,
this
.
taskList
=
new
TaskList
({
},
dataType
:
'
note
'
,
},
fieldName
:
'
note
'
,
components
:
{
selector
:
'
.notes
'
,
issueNoteEditedText
,
});
issueNoteAwardsList
,
}
issueNoteForm
,
},
},
handleFormUpdate
()
{
computed
:
{
this
.
formUpdateHandler
({
noteBody
()
{
note
:
this
.
$refs
.
noteForm
.
note
,
return
this
.
note
.
note
;
},
},
methods
:
{
renderGFM
()
{
$
(
this
.
$refs
[
'
note-body
'
]).
renderGFM
();
},
initTaskList
()
{
if
(
this
.
canEdit
)
{
this
.
taskList
=
new
TaskList
({
dataType
:
'
note
'
,
fieldName
:
'
note
'
,
selector
:
'
.notes
'
,
});
});
}
},
},
mounted
()
{
this
.
renderGFM
();
this
.
initTaskList
();
},
},
handleFormUpdate
()
{
updated
()
{
this
.
formUpdateHandler
({
this
.
initTaskList
();
note
:
this
.
$refs
.
noteForm
.
note
,
});
},
},
},
};
mounted
()
{
this
.
renderGFM
();
this
.
initTaskList
();
},
updated
()
{
this
.
initTaskList
();
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/issue_note_edited_text.vue
View file @
ffef1669
<
script
>
<
script
>
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
export
default
{
export
default
{
props
:
{
props
:
{
actionText
:
{
actionText
:
{
type
:
String
,
type
:
String
,
required
:
true
,
required
:
true
,
},
editedAt
:
{
type
:
String
,
required
:
true
,
},
editedBy
:
{
type
:
Object
,
required
:
true
,
},
className
:
{
type
:
String
,
required
:
false
,
default
:
'
edited-text
'
,
},
},
},
editedAt
:
{
components
:
{
type
:
String
,
timeAgoTooltip
,
required
:
true
,
},
},
editedBy
:
{
};
type
:
Object
,
required
:
true
,
},
className
:
{
type
:
String
,
required
:
false
,
default
:
'
edited-text
'
,
},
},
components
:
{
timeAgoTooltip
,
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -38,6 +38,7 @@ export default {
...
@@ -38,6 +38,7 @@ export default {
</a>
</a>
<time-ago-tooltip
<time-ago-tooltip
:time=
"editedAt"
:time=
"editedAt"
tooltip-placement=
"bottom"
/>
tooltip-placement=
"bottom"
/>
</div>
</div>
</
template
>
</
template
>
app/assets/javascripts/notes/components/issue_note_form.vue
View file @
ffef1669
<
script
>
<
script
>
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
markdownField
from
'
../../vue_shared/components/markdown/field.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
eventHub
from
'
../event_hub
'
;
export
default
{
export
default
{
props
:
{
props
:
{
noteBody
:
{
noteBody
:
{
type
:
String
,
type
:
String
,
required
:
false
,
required
:
false
,
default
:
''
,
default
:
''
,
},
noteId
:
{
type
:
Number
,
required
:
false
,
},
updateHandler
:
{
type
:
Function
,
required
:
true
,
},
cancelHandler
:
{
type
:
Function
,
required
:
true
,
},
saveButtonTitle
:
{
type
:
String
,
required
:
false
,
default
:
'
Save comment
'
,
},
},
},
noteId
:
{
data
()
{
type
:
Number
,
return
{
required
:
false
,
initialNote
:
this
.
noteBody
,
note
:
this
.
noteBody
,
markdownPreviewUrl
:
gl
.
issueData
.
preview_note_path
,
markdownDocsUrl
:
''
,
conflictWhileEditing
:
false
,
};
},
},
updateHandler
:
{
components
:
{
type
:
Function
,
markdownField
,
required
:
true
,
},
},
cancelHandler
:
{
computed
:
{
type
:
Function
,
isDirty
()
{
required
:
true
,
return
this
.
initialNote
!==
this
.
note
;
},
noteHash
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
},
},
saveButtonTitle
:
{
methods
:
{
type
:
String
,
handleUpdate
()
{
required
:
false
,
this
.
updateHandler
({
default
:
'
Save comment
'
,
note
:
this
.
note
,
},
});
},
},
data
()
{
editMyLastNote
()
{
return
{
if
(
this
.
note
===
''
)
{
initialNote
:
this
.
noteBody
,
const
discussion
=
$
(
this
.
$el
).
closest
(
'
.discussion-notes
'
);
note
:
this
.
noteBody
,
const
myLastNoteId
=
discussion
.
find
(
'
.js-my-note
'
).
last
().
attr
(
'
id
'
);
markdownPreviewUrl
:
gl
.
issueData
.
preview_note_path
,
markdownDocsUrl
:
''
,
conflictWhileEditing
:
false
,
};
},
components
:
{
markdownField
,
},
computed
:
{
isDirty
()
{
return
this
.
initialNote
!==
this
.
note
;
},
noteHash
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
},
methods
:
{
handleUpdate
()
{
this
.
updateHandler
({
note
:
this
.
note
,
});
},
editMyLastNote
()
{
if
(
this
.
note
===
''
)
{
const
discussion
=
$
(
this
.
$el
).
closest
(
'
.discussion-notes
'
);
const
myLastNoteId
=
discussion
.
find
(
'
.js-my-note
'
).
last
().
attr
(
'
id
'
);
if
(
myLastNoteId
)
{
if
(
myLastNoteId
)
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
eventHub
.
$emit
(
'
enterEditMode
'
,
{
noteId
:
parseInt
(
myLastNoteId
.
replace
(
'
note_
'
,
''
),
10
),
noteId
:
parseInt
(
myLastNoteId
.
replace
(
'
note_
'
,
''
),
10
),
});
});
}
}
}
}
}
,
},
},
},
mounted
()
{
mounted
()
{
const
issuableDataEl
=
document
.
getElementById
(
'
js-issuable-app-initial-data
'
);
const
issuableDataEl
=
document
.
getElementById
(
'
js-issuable-app-initial-data
'
);
const
issueData
=
JSON
.
parse
(
issuableDataEl
.
innerHTML
.
replace
(
/"/g
,
'
"
'
));
const
issueData
=
JSON
.
parse
(
issuableDataEl
.
innerHTML
.
replace
(
/"/g
,
'
"
'
));
this
.
markdownDocsUrl
=
issueData
.
markdownDocs
;
this
.
markdownDocsUrl
=
issueData
.
markdownDocs
;
this
.
$refs
.
textarea
.
focus
();
this
.
$refs
.
textarea
.
focus
();
},
},
watch
:
{
watch
:
{
noteBody
()
{
noteBody
()
{
if
(
this
.
note
===
this
.
initialNote
)
{
if
(
this
.
note
===
this
.
initialNote
)
{
this
.
note
=
this
.
noteBody
;
this
.
note
=
this
.
noteBody
;
}
else
{
}
else
{
this
.
conflictWhileEditing
=
true
;
this
.
conflictWhileEditing
=
true
;
}
}
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/issue_note_header.vue
View file @
ffef1669
<
script
>
<
script
>
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
import
{
mapMutations
}
from
'
vuex
'
;
import
timeAgoTooltip
from
'
../../vue_shared/components/time_ago_tooltip.vue
'
;
import
*
as
types
from
'
../stores/mutation_types
'
;
export
default
{
export
default
{
props
:
{
props
:
{
author
:
{
author
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
createdAt
:
{
type
:
String
,
required
:
true
,
},
actionText
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
actionTextHtml
:
{
type
:
String
,
required
:
false
,
default
:
''
,
},
noteId
:
{
type
:
Number
,
required
:
true
,
},
includeToggle
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
toggleHandler
:
{
type
:
Function
,
required
:
false
,
},
},
},
createdAt
:
{
data
()
{
type
:
String
,
return
{
required
:
true
,
isExpanded
:
true
,
};
},
},
actionText
:
{
components
:
{
type
:
String
,
timeAgoTooltip
,
required
:
false
,
default
:
''
,
},
},
actionTextHtml
:
{
computed
:
{
type
:
String
,
toggleChevronClass
()
{
required
:
false
,
return
this
.
isExpanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
default
:
''
,
},
noteTimestampLink
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
},
},
noteId
:
{
methods
:
{
type
:
Number
,
...
mapMutations
({
required
:
true
,
setTargetNoteHash
:
types
.
SET_TARGET_NOTE_HASH
,
}),
handleToggle
()
{
this
.
isExpanded
=
!
this
.
isExpanded
;
this
.
toggleHandler
();
},
updateTargetNoteHash
()
{
this
.
setTargetNoteHash
(
this
.
noteTimestampLink
);
},
},
},
includeToggle
:
{
};
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
toggleHandler
:
{
type
:
Function
,
required
:
false
,
},
},
data
()
{
return
{
isExpanded
:
true
,
};
},
components
:
{
timeAgoTooltip
,
},
computed
:
{
toggleChevronClass
()
{
return
this
.
isExpanded
?
'
fa-chevron-up
'
:
'
fa-chevron-down
'
;
},
noteTimestampLink
()
{
return
`#note_
${
this
.
noteId
}
`
;
},
},
methods
:
{
handleToggle
()
{
this
.
isExpanded
=
!
this
.
isExpanded
;
this
.
toggleHandler
();
},
updateTargetNoteHash
()
{
this
.
$store
.
commit
(
'
setTargetNoteHash
'
,
this
.
noteTimestampLink
);
},
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -81,13 +86,15 @@ export default {
...
@@ -81,13 +86,15 @@ export default {
<span
<span
v-if=
"actionTextHtml"
v-if=
"actionTextHtml"
v-html=
"actionTextHtml"
v-html=
"actionTextHtml"
class=
"system-note-message"
></span>
class=
"system-note-message"
>
</span>
<a
<a
:href=
"noteTimestampLink"
:href=
"noteTimestampLink"
@
click=
"updateTargetNoteHash"
>
@
click=
"updateTargetNoteHash"
>
<time-ago-tooltip
<time-ago-tooltip
:time=
"createdAt"
:time=
"createdAt"
tooltipPlacement=
"bottom"
/>
tooltipPlacement=
"bottom"
/>
</a>
</a>
</span>
</span>
</span>
</span>
...
@@ -101,7 +108,8 @@ export default {
...
@@ -101,7 +108,8 @@ export default {
<i
<i
:class=
"toggleChevronClass"
:class=
"toggleChevronClass"
class=
"fa"
class=
"fa"
aria-hidden=
"true"
></i>
aria-hidden=
"true"
>
</i>
Toggle discussion
Toggle discussion
</button>
</button>
</div>
</div>
...
...
app/assets/javascripts/notes/components/issue_note_signed_out_widget.vue
View file @
ffef1669
<
script
>
<
script
>
export
default
{
export
default
{
data
()
{
data
()
{
return
{
return
{
signInLink
:
'
#
'
,
signInLink
:
'
#
'
,
};
};
},
},
mounted
()
{
mounted
()
{
const
wrapper
=
document
.
querySelector
(
'
.js-notes-wrapper
'
);
const
wrapper
=
document
.
querySelector
(
'
.js-notes-wrapper
'
);
if
(
wrapper
)
{
if
(
wrapper
)
{
this
.
signInLink
=
wrapper
.
dataset
.
newSessionPath
;
this
.
signInLink
=
wrapper
.
dataset
.
newSessionPath
;
}
}
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/issue_notes.vue
View file @
ffef1669
<
script
>
<
script
>
/* global Flash */
/* global Flash */
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
{
mapGetters
,
mapActions
,
mapMutations
}
from
'
vuex
'
;
import
VueResource
from
'
vue-resource
'
;
import
store
from
'
../stores/
'
;
import
storeOptions
from
'
../stores/issue_notes_store
'
;
import
*
as
constants
from
'
../constants
'
import
eventHub
from
'
../event_hub
'
;
import
*
as
types
from
'
../stores/mutation_types
'
;
import
issueNote
from
'
./issue_note.vue
'
;
import
eventHub
from
'
../event_hub
'
;
import
issueDiscussion
from
'
./issue_discussion.vue
'
;
import
issueNote
from
'
./issue_note.vue
'
;
import
issueSystemNote
from
'
./issue_system_note.vue
'
;
import
issueDiscussion
from
'
./issue_discussion.vue
'
;
import
issueCommentForm
from
'
./issue_comment_form.vue
'
;
import
issueSystemNote
from
'
./issue_system_note.vue
'
;
import
placeholderNote
from
'
./issue_placeholder_note.vue
'
;
import
issueCommentForm
from
'
./issue_comment_form.vue
'
;
import
placeholderSystemNote
from
'
./issue_placeholder_system_note.vue
'
;
import
placeholderNote
from
'
./issue_placeholder_note.vue
'
;
import
store
from
'
./store
'
;
import
placeholderSystemNote
from
'
./issue_placeholder_system_note.vue
'
;
import
loadingIcon
from
'
../../vue_shared/components/loading_icon.vue
'
;
export
default
{
export
default
{
name
:
'
IssueNotes
'
,
name
:
'
IssueNotes
'
,
store
,
store
,
data
()
{
data
()
{
return
{
return
{
isLoading
:
true
,
isLoading
:
true
,
};
};
},
components
:
{
issueNote
,
issueDiscussion
,
issueSystemNote
,
issueCommentForm
,
placeholderNote
,
placeholderSystemNote
,
},
computed
:
{
...
Vuex
.
mapGetters
([
'
notes
'
,
'
notesById
'
,
]),
},
methods
:
{
componentName
(
note
)
{
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
'
systemNote
'
)
{
return
placeholderSystemNote
;
}
return
placeholderNote
;
}
else
if
(
note
.
individual_note
)
{
return
note
.
notes
[
0
].
system
?
issueSystemNote
:
issueNote
;
}
return
issueDiscussion
;
},
},
componentData
(
note
)
{
components
:
{
return
note
.
individual_note
?
note
.
notes
[
0
]
:
note
;
issueNote
,
issueDiscussion
,
issueSystemNote
,
issueCommentForm
,
loadingIcon
,
placeholderNote
,
placeholderSystemNote
,
},
},
fetchNotes
()
{
computed
:
{
const
{
discussionsPath
}
=
this
.
$el
.
parentNode
.
dataset
;
...
mapGetters
([
'
notes
'
,
'
notesById
'
,
]),
},
methods
:
{
...
mapActions
({
actionFetchNotes
:
'
fetchNotes
'
,
}),
...
mapActions
([
'
poll
'
,
'
toggleAward
'
,
'
scrollToNoteIfNeeded
'
,
]),
...
mapMutations
({
setLastFetchedAt
:
types
.
SET_LAST_FETCHED_AT
,
setTargetNoteHash
:
types
.
SET_TARGET_NOTE_HASH
,
}),
getComponentName
(
note
)
{
if
(
note
.
isPlaceholderNote
)
{
if
(
note
.
placeholderType
===
constants
.
SYSTEM_NOTE
)
{
return
placeholderSystemNote
;
}
return
placeholderNote
;
}
else
if
(
note
.
individual_note
)
{
return
note
.
notes
[
0
].
system
?
issueSystemNote
:
issueNote
;
}
this
.
$store
.
dispatch
(
'
fetchNotes
'
,
discussionsPath
)
return
issueDiscussion
;
.
then
(()
=>
{
},
this
.
isLoading
=
false
;
getComponentData
(
note
)
{
return
note
.
individual_note
?
note
.
notes
[
0
]
:
note
;
},
fetchNotes
()
{
const
{
discussionsPath
}
=
this
.
$el
.
parentNode
.
dataset
;
// Scroll to note if we have hash fragment in the page URL
this
.
actionFetchNotes
(
discussionsPath
)
Vue
.
nextTick
(()
=>
{
.
then
(()
=>
{
this
.
checkLocationHash
();
this
.
isLoading
=
false
;
});
})
.
catch
(()
=>
{
Flash
(
'
Something went wrong while fetching issue comments. Please try again.
'
);
});
},
initPolling
()
{
const
{
lastFetchedAt
}
=
$
(
'
.js-notes-wrapper
'
)[
0
].
dataset
;
this
.
$store
.
commit
(
'
setLastFetchedAt
'
,
lastFetchedAt
);
// FIXME: @fatihacet Implement real polling mechanism
// Scroll to note if we have hash fragment in the page URL
setInterval
(()
=>
{
Vue
.
nextTick
(()
=>
{
this
.
$store
.
dispatch
(
'
poll
'
)
this
.
checkLocationHash
();
.
then
((
res
)
=>
{
});
this
.
$store
.
commit
(
'
setLastFetchedAt
'
,
res
.
lastFetchedAt
);
})
})
.
catch
(()
=>
{
.
catch
(()
=>
{
Flash
(
'
Something went wrong while fetching
latest comments
.
'
);
Flash
(
'
Something went wrong while fetching
issue comments. Please try again
.
'
);
});
});
},
15000
);
},
},
initPolling
()
{
bindEventHubListeners
()
{
const
{
lastFetchedAt
}
=
$
(
'
.js-notes-wrapper
'
)[
0
].
dataset
;
eventHub
.
$on
(
'
toggleAward
'
,
(
data
)
=>
{
this
.
setLastFetchedAt
(
lastFetchedAt
);
const
{
awardName
,
noteId
}
=
data
;
const
endpoint
=
this
.
notesById
[
noteId
].
toggle_award_path
;
this
.
$store
.
dispatch
(
'
toggleAward
'
,
{
endpoint
,
awardName
,
noteId
})
// FIXME: @fatihacet Implement real polling mechanism
.
catch
(()
=>
{
setInterval
(()
=>
{
Flash
(
'
Something went wrong on our end.
'
);
this
.
poll
()
});
.
then
((
res
)
=>
{
});
this
.
setLastFetchedAt
(
res
.
lastFetchedAt
);
})
.
catch
(()
=>
{
Flash
(
'
Something went wrong while fetching latest comments.
'
);
});
},
15000
);
},
bindEventHubListeners
()
{
eventHub
.
$on
(
'
toggleAward
'
,
(
data
)
=>
{
const
{
awardName
,
noteId
}
=
data
;
const
endpoint
=
this
.
notesById
[
noteId
].
toggle_award_path
;
$
(
document
).
on
(
'
issuable:change
'
,
(
e
,
isClosed
)
=>
{
this
.
toggleAward
({
endpoint
,
awardName
,
noteId
})
eventHub
.
$emit
(
'
issueStateChanged
'
,
isClosed
);
.
catch
(()
=>
{
new
Flash
(
'
Something went wrong on our end.
'
)});
});
});
},
checkLocationHash
()
{
const
hash
=
gl
.
utils
.
getLocationHash
();
const
$el
=
$
(
`#
${
hash
}
`
);
if
(
hash
&&
$el
)
{
$
(
document
).
on
(
'
issuable:change
'
,
(
e
,
isClosed
)
=>
{
this
.
$store
.
commit
(
'
setTargetNoteHash
'
,
hash
);
eventHub
.
$emit
(
'
issueStateChanged
'
,
isClosed
);
this
.
$store
.
dispatch
(
'
scrollToNoteIfNeeded
'
,
$el
);
});
}
},
checkLocationHash
()
{
const
hash
=
gl
.
utils
.
getLocationHash
();
const
$el
=
$
(
`#
${
hash
}
`
);
if
(
hash
&&
$el
)
{
this
.
setTargetNoteHash
(
hash
);
this
.
scrollToNoteIfNeeded
(
$el
);
}
},
},
mounted
()
{
this
.
fetchNotes
();
this
.
initPolling
();
this
.
bindEventHubListeners
();
},
},
},
};
mounted
()
{
this
.
fetchNotes
();
this
.
initPolling
();
this
.
bindEventHubListeners
();
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -121,9 +133,7 @@ export default {
...
@@ -121,9 +133,7 @@ export default {
<div
<div
v-if=
"isLoading"
v-if=
"isLoading"
class=
"loading"
>
class=
"loading"
>
<i
<loading-icon
/>
class=
"fa fa-spinner fa-spin"
aria-hidden=
"true"
></i>
</div>
</div>
<ul
<ul
v-if=
"!isLoading"
v-if=
"!isLoading"
...
@@ -131,9 +141,10 @@ export default {
...
@@ -131,9 +141,10 @@ export default {
class=
"notes main-notes-list timeline"
>
class=
"notes main-notes-list timeline"
>
<component
<component
v-for=
"note in notes"
v-for=
"note in notes"
:is=
"componentName(note)"
:is=
"getComponentName(note)"
:note=
"componentData(note)"
:note=
"getComponentData(note)"
:key=
"note.id"
/>
:key=
"note.id"
/>
</ul>
</ul>
<issue-comment-form
v-if=
"!isLoading"
/>
<issue-comment-form
v-if=
"!isLoading"
/>
</div>
</div>
...
...
app/assets/javascripts/notes/components/issue_placeholder_note.vue
View file @
ffef1669
<
script
>
<
script
>
export
default
{
export
default
{
props
:
{
props
:
{
note
:
{
note
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
},
},
data
()
{
data
()
{
return
{
return
{
currentUser
:
window
.
gl
.
currentUserData
,
currentUser
:
window
.
gl
.
currentUserData
,
};
};
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
...
@@ -21,7 +21,8 @@ export default {
...
@@ -21,7 +21,8 @@ export default {
<a
:href=
"currentUser.path"
>
<a
:href=
"currentUser.path"
>
<img
<img
:src=
"currentUser.avatar_url"
:src=
"currentUser.avatar_url"
class=
"avatar s40"
/>
class=
"avatar s40"
/>
</a>
</a>
</div>
</div>
<div
<div
...
...
app/assets/javascripts/notes/components/issue_placeholder_system_note.vue
View file @
ffef1669
<
script
>
<
script
>
export
default
{
export
default
{
props
:
{
props
:
{
note
:
{
note
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
},
},
};
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/components/issue_system_note.vue
View file @
ffef1669
<
script
>
<
script
>
import
{
mapGetters
}
from
'
vuex
'
;
import
{
mapGetters
}
from
'
vuex
'
;
import
iconsMap
from
'
./issue_note_icons
'
;
import
iconsMap
from
'
./issue_note_icons
'
;
import
issueNoteHeader
from
'
./issue_note_header.vue
'
;
import
issueNoteHeader
from
'
./issue_note_header.vue
'
;
export
default
{
export
default
{
props
:
{
props
:
{
note
:
{
note
:
{
type
:
Object
,
type
:
Object
,
required
:
true
,
required
:
true
,
},
},
},
},
data
()
{
data
()
{
return
{
return
{
svg
:
iconsMap
[
this
.
note
.
system_note_icon_name
],
svg
:
iconsMap
[
this
.
note
.
system_note_icon_name
],
};
};
},
components
:
{
issueNoteHeader
,
},
computed
:
{
...
mapGetters
([
'
targetNoteHash
'
,
]),
noteAnchorId
()
{
return
`note_
${
this
.
note
.
id
}
`
;
},
},
isTargetNote
()
{
components
:
{
return
this
.
targetNoteHash
===
this
.
noteAnchorId
;
issueNoteHeader
,
},
},
},
computed
:
{
};
...
mapGetters
([
'
targetNoteHash
'
,
]),
noteAnchorId
()
{
return
`note_
${
this
.
note
.
id
}
`
;
},
isTargetNote
()
{
return
this
.
targetNoteHash
===
this
.
noteAnchorId
;
},
},
};
</
script
>
</
script
>
<
template
>
<
template
>
...
...
app/assets/javascripts/notes/constants.js
View file @
ffef1669
...
@@ -5,4 +5,4 @@ export const SYSTEM_NOTE = 'systemNote';
...
@@ -5,4 +5,4 @@ export const SYSTEM_NOTE = 'systemNote';
export
const
COMMENT
=
'
comment
'
;
export
const
COMMENT
=
'
comment
'
;
export
const
OPENED
=
'
opened
'
;
export
const
OPENED
=
'
opened
'
;
export
const
REOPENED
=
'
reopened
'
;
export
const
REOPENED
=
'
reopened
'
;
export
const
CLOSED
=
'
closed
'
;
export
const
CLOSED
=
'
closed
'
;
\ No newline at end of file
app/assets/javascripts/notes/stores/actions.js
View file @
ffef1669
...
@@ -138,8 +138,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
...
@@ -138,8 +138,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
export
const
poll
=
({
commit
,
state
,
getters
})
=>
{
export
const
poll
=
({
commit
,
state
,
getters
})
=>
{
const
{
notesPath
}
=
$
(
'
.js-notes-wrapper
'
)[
0
].
dataset
;
const
{
notesPath
}
=
$
(
'
.js-notes-wrapper
'
)[
0
].
dataset
;
return
service
return
service
.
poll
(
`
${
notesPath
}
?full_data=1`
,
state
.
lastFetchedAt
)
.
poll
(
`
${
notesPath
}
?full_data=1`
,
state
.
lastFetchedAt
)
.
then
(
res
=>
res
.
json
())
.
then
(
res
=>
res
.
json
())
.
then
((
res
)
=>
{
.
then
((
res
)
=>
{
if
(
res
.
notes
.
length
)
{
if
(
res
.
notes
.
length
)
{
...
@@ -188,8 +187,8 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => {
...
@@ -188,8 +187,8 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => {
});
});
if
(
amIAwarded
)
{
if
(
amIAwarded
)
{
Object
.
assign
(
data
,
{
awardName
:
counterAward
})
;
data
.
awardName
=
counterAward
;
Object
.
assign
(
data
,
{
skipMutalityCheck
:
true
})
;
data
.
skipMutalityCheck
=
true
;
dispatch
(
types
.
TOGGLE_AWARD
,
data
);
dispatch
(
types
.
TOGGLE_AWARD
,
data
);
}
}
...
...
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