Skip to content
Snippets Groups Projects
Commit ffef1669 authored by Filipa Lacerda's avatar Filipa Lacerda
Browse files

Use mapActions, mapGetters and mapMutations for components

parent 4e81ad2a
No related branches found
No related tags found
No related merge requests found
Showing
with 850 additions and 804 deletions
<script>
/* global Flash */
 
import { mapActions } from 'vuex';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
Loading
Loading
@@ -60,6 +61,9 @@
},
},
methods: {
...mapActions([
'saveNote'
]),
handleSave(withIssueAction) {
if (this.note.length) {
const noteData = {
Loading
Loading
@@ -79,7 +83,7 @@
noteData.data.note.type = constants.DISCUSSION_NOTE;
}
 
this.$store.dispatch('saveNote', noteData)
this.saveNote(noteData)
.then((res) => {
if (res.errors) {
if (res.errors.commands_only) {
Loading
Loading
<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';
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';
export default {
props: {
note: {
type: Object,
required: true,
export default {
props: {
note: {
type: Object,
required: true,
},
},
},
data() {
return {
newNotePath: window.gl.issueData.create_note_path,
isReplying: false,
};
},
components: {
issueNote,
userAvatarLink,
issueNoteHeader,
issueNoteActions,
issueNoteSignedOutWidget,
issueNoteEditedText,
issueNoteForm,
placeholderNote,
placeholderSystemNote,
},
computed: {
discussion() {
return this.note.notes[0];
data() {
return {
newNotePath: window.gl.issueData.create_note_path,
isReplying: false,
};
},
author() {
return this.discussion.author;
components: {
issueNote,
userAvatarLink,
issueNoteHeader,
issueNoteActions,
issueNoteSignedOutWidget,
issueNoteEditedText,
issueNoteForm,
placeholderNote,
placeholderSystemNote,
},
canReply() {
return window.gl.issueData.current_user.can_create_note;
computed: {
discussion() {
return this.note.notes[0];
},
author() {
return this.discussion.author;
},
canReply() {
return window.gl.issueData.current_user.can_create_note;
},
},
},
methods: {
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === 'systemNote') {
return placeholderSystemNote;
methods: {
...mapActions([
'saveNote',
]),
...mapMutations({
toggleDiscussion: TOGGLE_DISCUSSION,
}),
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
}
return placeholderNote;
}
 
return issueNote;
},
componentData(note) {
return note.isPlaceholderNote ? note.notes[0] : note;
},
toggleDiscussion() {
this.$store.commit('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?';
// eslint-disable-next-line no-alert
const isConfirmed = confirm(msg);
if (!isConfirmed) {
return;
return issueNote;
},
componentData(note) {
return note.isPlaceholderNote ? note.notes[0] : note;
},
toggleDiscussion() {
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?';
// eslint-disable-next-line no-alert
const isConfirmed = confirm(msg);
if (!isConfirmed) {
return;
}
}
}
 
this.isReplying = false;
},
saveReply({ note }) {
const replyData = {
endpoint: this.newNotePath,
flashContainer: this.$el,
data: {
in_reply_to_discussion_id: this.note.reply_id,
target_type: 'issue',
target_id: this.discussion.noteable_id,
note: { note },
full_data: true,
},
};
this.isReplying = false;
},
saveReply({ note }) {
const replyData = {
endpoint: this.newNotePath,
flashContainer: this.$el,
data: {
in_reply_to_discussion_id: this.note.reply_id,
target_type: 'issue',
target_id: this.discussion.noteable_id,
note: { note },
full_data: true,
},
};
 
this.$store.dispatch('saveNote', replyData)
.then(() => {
this.isReplying = false;
})
.catch(() => {
Flash('Something went wrong while adding your reply. Please try again.');
});
this.saveNote(replyData)
.then(() => {
this.isReplying = false;
})
.catch(() => Flash('Something went wrong while adding your reply. Please try again.'));
},
},
},
};
};
</script>
 
<template>
Loading
Loading
@@ -132,8 +136,7 @@ export default {
:edited-at="note.last_updated_at"
:edited-by="note.last_updated_by"
actionText="Last updated"
className="discussion-headline-light js-discussion-headline"
/>
className="discussion-headline-light js-discussion-headline" />
</div>
</div>
<div
Loading
Loading
@@ -162,7 +165,8 @@ export default {
saveButtonTitle="Comment"
:update-handler="saveReply"
:cancel-handler="cancelReplyForm"
ref="noteForm" />
ref="noteForm"
/>
<issue-note-signed-out-widget v-if="!canReply" />
</div>
</div>
Loading
Loading
<script>
/* global Flash */
/* global Flash */
 
import { mapGetters } from 'vuex';
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 issueNoteBody from './issue_note_body.vue';
import eventHub from '../event_hub';
import { mapGetters, mapActions } from 'vuex';
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 issueNoteBody from './issue_note_body.vue';
import eventHub from '../event_hub';
 
export default {
props: {
note: {
type: Object,
required: true,
export default {
props: {
note: {
type: Object,
required: true,
},
},
},
data() {
return {
isEditing: false,
isDeleting: false,
};
},
components: {
userAvatarLink,
issueNoteHeader,
issueNoteActions,
issueNoteBody,
},
computed: {
...mapGetters([
'targetNoteHash',
]),
author() {
return this.note.author;
},
classNameBindings() {
data() {
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,
isEditing: false,
isDeleting: false,
};
},
canReportAsAbuse() {
return this.note.report_abuse_path && this.author.id !== window.gon.current_user_id;
},
noteAnchorId() {
return `note_${this.note.id}`;
components: {
userAvatarLink,
issueNoteHeader,
issueNoteActions,
issueNoteBody,
},
},
methods: {
editHandler() {
this.isEditing = true;
computed: {
...mapGetters([
'targetNoteHash',
]),
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() {
const msg = 'Are you sure you want to delete this list?';
const isConfirmed = confirm(msg); // eslint-disable-line
methods: {
...mapActions([
'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.isDeleting = true;
this.$store
.dispatch('deleteNote', this.note)
this.updateNote(data)
.then(() => {
this.isDeleting = false;
this.isEditing = false;
$(this.$refs.noteBody.$el).renderGFM();
})
.catch(() => {
new Flash('Something went wrong while deleting your note. Please try again.'); // eslint-disable-line
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,
},
};
.catch(() => Flash('Something went wrong while editing your comment. Please try again.'));
},
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
if (!isConfirmed) {
return;
}
}
 
this.$store.dispatch('updateNote', data)
.then(() => {
this.isEditing = false;
$(this.$refs.noteBody.$el).renderGFM();
})
.catch(() => {
Flash('Something went wrong while editing your comment. Please try again.');
});
this.isEditing = 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
if (!isConfirmed) {
return;
created() {
eventHub.$on('enterEditMode', ({ noteId }) => {
if (noteId === this.note.id) {
this.isEditing = true;
this.scrollToNoteIfNeeded($(this.$el));
}
}
this.isEditing = false;
});
},
},
created() {
eventHub.$on('enterEditMode', ({ noteId }) => {
if (noteId === this.note.id) {
this.isEditing = true;
this.$store.dispatch('scrollToNoteIfNeeded', $(this.$el));
}
});
},
};
};
</script>
 
<template>
Loading
Loading
@@ -124,7 +126,8 @@ export default {
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40" />
:img-size="40"
/>
</div>
<div class="timeline-content">
<div class="note-header">
Loading
Loading
@@ -132,7 +135,8 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
actionText="commented" />
actionText="commented"
/>
<issue-note-actions
:author-id="author.id"
:note-id="note.id"
Loading
Loading
@@ -142,7 +146,8 @@ export default {
:can-report-as-abuse="canReportAsAbuse"
:report-abuse-path="note.report_abuse_path"
:edit-handler="editHandler"
:delete-handler="deleteHandler" />
:delete-handler="deleteHandler"
/>
</div>
<issue-note-body
:note="note"
Loading
Loading
@@ -150,7 +155,8 @@ export default {
:is-editing="isEditing"
:form-update-handler="formUpdateHandler"
:form-cancel-handler="formCancelHandler"
ref="noteBody" />
ref="noteBody"
/>
</div>
</div>
</li>
Loading
Loading
<script>
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import loadingIcon from '../../vue_shared/components/loadingIcon.vue';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
 
export default {
props: {
authorId: {
type: Number,
required: true,
export default {
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,
},
canReportAsAbuse: {
type: Boolean,
required: true,
},
editHandler: {
type: Function,
required: true,
},
deleteHandler: {
type: Function,
required: true,
},
},
noteId: {
type: Number,
required: true,
data() {
return {
emojiSmiling,
emojiSmile,
emojiSmiley,
};
},
accessLevel: {
type: String,
required: false,
default: '',
components: {
loadingIcon,
},
reportAbusePath: {
type: String,
required: true,
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;
},
},
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>
 
<template>
Loading
Loading
@@ -82,13 +85,16 @@ export default {
<loading-icon />
<span
v-html="emojiSmiling"
class="link-highlight award-control-icon-neutral"></span>
class="link-highlight award-control-icon-neutral">
</span>
<span
v-html="emojiSmiley"
class="link-highlight award-control-icon-positive"></span>
class="link-highlight award-control-icon-positive">
</span>
<span
v-html="emojiSmile"
class="link-highlight award-control-icon-super-positive"></span>
class="link-highlight award-control-icon-super-positive">
</span>
</a>
<div
v-if="shouldShowActionsDropdown"
Loading
Loading
@@ -101,7 +107,8 @@ export default {
data-container="body">
<i
aria-hidden="true"
class="fa fa-ellipsis-v icon"></i>
class="fa fa-ellipsis-v icon">
</i>
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<template v-if="canEdit">
Loading
Loading
<script>
/* global Flash */
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import * as Emoji from '../../emoji';
export default {
props: {
awards: {
type: Array,
required: true,
/* global Flash */
import { mapActions } 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 * as Emoji from '../../emoji';
export default {
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,
},
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) {
data() {
const userId = window.gon.current_user_id;
return {
active: this.amIAwarded(awardList),
disabled: !this.canInteractWithEmoji(awardList, awardName),
emojiSmiling,
emojiSmile,
emojiSmiley,
canAward: !!userId,
myUserId: userId,
};
},
canInteractWithEmoji(awardList, awardName) {
let isAllowed = true;
const restrictedEmojis = ['thumbsup', 'thumbsdown'];
const { myUserId, noteAuthorId } = this;
// Users can not add :+1: and :-1: to their notes
if (myUserId === noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) {
isAllowed = false;
}
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);
});
 
return this.canAward && isAllowed;
},
amIAwarded(awardList) {
const isAwarded = awardList.filter(award => award.user.id === this.myUserId);
// 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 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;
return orderedAwards;
},
isAuthoredByMe() {
return this.noteAuthorId === window.gon.current_user_id;
},
},
handleAward(awardName) {
const data = {
endpoint: this.toggleAwardPath,
noteId: this.noteId,
awardName,
};
this.$store.dispatch('toggleAward', data)
.then(() => {
$(this.$el).find('.award-control').tooltip('fixTitle');
})
.catch(() => {
Flash('Something went wrong on our end.');
});
methods: {
...mapActions([
'toggleAward',
]),
getAwardHTML(name) {
return Emoji.glEmojiTag(name);
},
getAwardClassBindings(awardList, awardName) {
return {
active: this.amIAwarded(awardList),
disabled: !this.canInteractWithEmoji(awardList, awardName),
};
},
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>
 
<template>
Loading
Loading
@@ -189,13 +191,16 @@ export default {
type="button">
<span
v-html="emojiSmiling"
class="award-control-icon award-control-icon-neutral"></span>
class="award-control-icon award-control-icon-neutral">
</span>
<span
v-html="emojiSmiley"
class="award-control-icon award-control-icon-positive"></span>
class="award-control-icon award-control-icon-positive">
</span>
<span
v-html="emojiSmile"
class="award-control-icon award-control-icon-super-positive"></span>
class="award-control-icon award-control-icon-super-positive">
</span>
<i
aria-hidden="true"
class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>
Loading
Loading
<script>
import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteAwardsList from './issue_note_awards_list.vue';
import issueNoteForm from './issue_note_form.vue';
import TaskList from '../../task_list';
import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteAwardsList from './issue_note_awards_list.vue';
import issueNoteForm from './issue_note_form.vue';
import TaskList from '../../task_list';
 
export default {
props: {
note: {
type: Object,
required: true,
export default {
props: {
note: {
type: Object,
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: {
type: Boolean,
required: true,
components: {
issueNoteEditedText,
issueNoteAwardsList,
issueNoteForm,
},
isEditing: {
type: Boolean,
required: false,
default: false,
computed: {
noteBody() {
return this.note.note;
},
},
formUpdateHandler: {
type: Function,
required: true,
},
formCancelHandler: {
type: Function,
required: true,
},
},
components: {
issueNoteEditedText,
issueNoteAwardsList,
issueNoteForm,
},
computed: {
noteBody() {
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',
methods: {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
},
initTaskList() {
if (this.canEdit) {
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes',
});
}
},
handleFormUpdate() {
this.formUpdateHandler({
note: this.$refs.noteForm.note,
});
}
},
},
mounted() {
this.renderGFM();
this.initTaskList();
},
handleFormUpdate() {
this.formUpdateHandler({
note: this.$refs.noteForm.note,
});
updated() {
this.initTaskList();
},
},
mounted() {
this.renderGFM();
this.initTaskList();
},
updated() {
this.initTaskList();
},
};
};
</script>
 
<template>
Loading
Loading
<script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
 
export default {
props: {
actionText: {
type: String,
required: true,
export default {
props: {
actionText: {
type: String,
required: true,
},
editedAt: {
type: String,
required: true,
},
editedBy: {
type: Object,
required: true,
},
className: {
type: String,
required: false,
default: 'edited-text',
},
},
editedAt: {
type: String,
required: true,
components: {
timeAgoTooltip,
},
editedBy: {
type: Object,
required: true,
},
className: {
type: String,
required: false,
default: 'edited-text',
},
},
components: {
timeAgoTooltip,
},
};
};
</script>
 
<template>
Loading
Loading
@@ -38,6 +38,7 @@ export default {
</a>
<time-ago-tooltip
:time="editedAt"
tooltip-placement="bottom" />
tooltip-placement="bottom"
/>
</div>
</template>
<script>
import markdownField from '../../vue_shared/components/markdown/field.vue';
import eventHub from '../event_hub';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import eventHub from '../event_hub';
 
export default {
props: {
noteBody: {
type: String,
required: false,
default: '',
export default {
props: {
noteBody: {
type: String,
required: false,
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: {
type: Number,
required: false,
data() {
return {
initialNote: this.noteBody,
note: this.noteBody,
markdownPreviewUrl: gl.issueData.preview_note_path,
markdownDocsUrl: '',
conflictWhileEditing: false,
};
},
updateHandler: {
type: Function,
required: true,
components: {
markdownField,
},
cancelHandler: {
type: Function,
required: true,
computed: {
isDirty() {
return this.initialNote !== this.note;
},
noteHash() {
return `#note_${this.noteId}`;
},
},
saveButtonTitle: {
type: String,
required: false,
default: 'Save comment',
},
},
data() {
return {
initialNote: this.noteBody,
note: this.noteBody,
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');
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) {
eventHub.$emit('enterEditMode', {
noteId: parseInt(myLastNoteId.replace('note_', ''), 10),
});
if (myLastNoteId) {
eventHub.$emit('enterEditMode', {
noteId: parseInt(myLastNoteId.replace('note_', ''), 10),
});
}
}
}
},
},
},
mounted() {
const issuableDataEl = document.getElementById('js-issuable-app-initial-data');
const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/&quot;/g, '"'));
mounted() {
const issuableDataEl = document.getElementById('js-issuable-app-initial-data');
const issueData = JSON.parse(issuableDataEl.innerHTML.replace(/&quot;/g, '"'));
 
this.markdownDocsUrl = issueData.markdownDocs;
this.$refs.textarea.focus();
},
watch: {
noteBody() {
if (this.note === this.initialNote) {
this.note = this.noteBody;
} else {
this.conflictWhileEditing = true;
}
this.markdownDocsUrl = issueData.markdownDocs;
this.$refs.textarea.focus();
},
watch: {
noteBody() {
if (this.note === this.initialNote) {
this.note = this.noteBody;
} else {
this.conflictWhileEditing = true;
}
},
},
},
};
};
</script>
 
<template>
Loading
Loading
<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 {
props: {
author: {
type: Object,
required: true,
export default {
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,
},
toggleHandler: {
type: Function,
required: false,
},
},
createdAt: {
type: String,
required: true,
data() {
return {
isExpanded: true,
};
},
actionText: {
type: String,
required: false,
default: '',
components: {
timeAgoTooltip,
},
actionTextHtml: {
type: String,
required: false,
default: '',
computed: {
toggleChevronClass() {
return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down';
},
noteTimestampLink() {
return `#note_${this.noteId}`;
},
},
noteId: {
type: Number,
required: true,
methods: {
...mapMutations({
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>
 
<template>
Loading
Loading
@@ -81,13 +86,15 @@ export default {
<span
v-if="actionTextHtml"
v-html="actionTextHtml"
class="system-note-message"></span>
class="system-note-message">
</span>
<a
:href="noteTimestampLink"
@click="updateTargetNoteHash">
<time-ago-tooltip
:time="createdAt"
tooltipPlacement="bottom" />
tooltipPlacement="bottom"
/>
</a>
</span>
</span>
Loading
Loading
@@ -101,7 +108,8 @@ export default {
<i
:class="toggleChevronClass"
class="fa"
aria-hidden="true"></i>
aria-hidden="true">
</i>
Toggle discussion
</button>
</div>
Loading
Loading
<script>
export default {
data() {
return {
signInLink: '#',
};
},
mounted() {
const wrapper = document.querySelector('.js-notes-wrapper');
export default {
data() {
return {
signInLink: '#',
};
},
mounted() {
const wrapper = document.querySelector('.js-notes-wrapper');
 
if (wrapper) {
this.signInLink = wrapper.dataset.newSessionPath;
}
},
};
if (wrapper) {
this.signInLink = wrapper.dataset.newSessionPath;
}
},
};
</script>
 
<template>
Loading
Loading
<script>
/* global Flash */
/* global Flash */
 
import Vue from 'vue';
import Vuex from 'vuex';
import VueResource from 'vue-resource';
import storeOptions from '../stores/issue_notes_store';
import eventHub from '../event_hub';
import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue';
import issueSystemNote from './issue_system_note.vue';
import issueCommentForm from './issue_comment_form.vue';
import placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
import store from './store';
import Vue from 'vue';
import { mapGetters, mapActions, mapMutations } from 'vuex';
import store from '../stores/';
import * as constants from '../constants'
import * as types from '../stores/mutation_types';
import eventHub from '../event_hub';
import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue';
import issueSystemNote from './issue_system_note.vue';
import issueCommentForm from './issue_comment_form.vue';
import placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
 
export default {
name: 'IssueNotes',
store,
data() {
return {
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;
export default {
name: 'IssueNotes',
store,
data() {
return {
isLoading: true,
};
},
componentData(note) {
return note.individual_note ? note.notes[0] : note;
components: {
issueNote,
issueDiscussion,
issueSystemNote,
issueCommentForm,
loadingIcon,
placeholderNote,
placeholderSystemNote,
},
fetchNotes() {
const { discussionsPath } = this.$el.parentNode.dataset;
computed: {
...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)
.then(() => {
this.isLoading = false;
return issueDiscussion;
},
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
Vue.nextTick(() => {
this.checkLocationHash();
});
})
.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);
this.actionFetchNotes(discussionsPath)
.then(() => {
this.isLoading = false;
 
// FIXME: @fatihacet Implement real polling mechanism
setInterval(() => {
this.$store.dispatch('poll')
.then((res) => {
this.$store.commit('setLastFetchedAt', res.lastFetchedAt);
// Scroll to note if we have hash fragment in the page URL
Vue.nextTick(() => {
this.checkLocationHash();
});
})
.catch(() => {
Flash('Something went wrong while fetching latest comments.');
Flash('Something went wrong while fetching issue comments. Please try again.');
});
}, 15000);
},
bindEventHubListeners() {
eventHub.$on('toggleAward', (data) => {
const { awardName, noteId } = data;
const endpoint = this.notesById[noteId].toggle_award_path;
},
initPolling() {
const { lastFetchedAt } = $('.js-notes-wrapper')[0].dataset;
this.setLastFetchedAt(lastFetchedAt);
 
this.$store.dispatch('toggleAward', { endpoint, awardName, noteId })
.catch(() => {
Flash('Something went wrong on our end.');
});
});
// FIXME: @fatihacet Implement real polling mechanism
setInterval(() => {
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) => {
eventHub.$emit('issueStateChanged', isClosed);
});
},
checkLocationHash() {
const hash = gl.utils.getLocationHash();
const $el = $(`#${hash}`);
this.toggleAward({ endpoint, awardName, noteId })
.catch(() => {new Flash('Something went wrong on our end.')});
});
 
if (hash && $el) {
this.$store.commit('setTargetNoteHash', hash);
this.$store.dispatch('scrollToNoteIfNeeded', $el);
}
$(document).on('issuable:change', (e, isClosed) => {
eventHub.$emit('issueStateChanged', isClosed);
});
},
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>
 
<template>
Loading
Loading
@@ -121,9 +133,7 @@ export default {
<div
v-if="isLoading"
class="loading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"></i>
<loading-icon />
</div>
<ul
v-if="!isLoading"
Loading
Loading
@@ -131,9 +141,10 @@ export default {
class="notes main-notes-list timeline">
<component
v-for="note in notes"
:is="componentName(note)"
:note="componentData(note)"
:key="note.id" />
:is="getComponentName(note)"
:note="getComponentData(note)"
:key="note.id"
/>
</ul>
<issue-comment-form v-if="!isLoading" />
</div>
Loading
Loading
<script>
export default {
props: {
note: {
type: Object,
required: true,
export default {
props: {
note: {
type: Object,
required: true,
},
},
},
data() {
return {
currentUser: window.gl.currentUserData,
};
},
};
data() {
return {
currentUser: window.gl.currentUserData,
};
},
};
</script>
 
<template>
Loading
Loading
@@ -21,7 +21,8 @@ export default {
<a :href="currentUser.path">
<img
:src="currentUser.avatar_url"
class="avatar s40" />
class="avatar s40"
/>
</a>
</div>
<div
Loading
Loading
<script>
export default {
props: {
note: {
type: Object,
required: true,
export default {
props: {
note: {
type: Object,
required: true,
},
},
},
};
};
</script>
 
<template>
Loading
Loading
<script>
import { mapGetters } from 'vuex';
import iconsMap from './issue_note_icons';
import issueNoteHeader from './issue_note_header.vue';
import { mapGetters } from 'vuex';
import iconsMap from './issue_note_icons';
import issueNoteHeader from './issue_note_header.vue';
 
export default {
props: {
note: {
type: Object,
required: true,
export default {
props: {
note: {
type: Object,
required: true,
},
},
},
data() {
return {
svg: iconsMap[this.note.system_note_icon_name],
};
},
components: {
issueNoteHeader,
},
computed: {
...mapGetters([
'targetNoteHash',
]),
noteAnchorId() {
return `note_${this.note.id}`;
data() {
return {
svg: iconsMap[this.note.system_note_icon_name],
};
},
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
components: {
issueNoteHeader,
},
},
};
computed: {
...mapGetters([
'targetNoteHash',
]),
noteAnchorId() {
return `note_${this.note.id}`;
},
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
},
};
</script>
 
<template>
Loading
Loading
Loading
Loading
@@ -5,4 +5,4 @@ export const SYSTEM_NOTE = 'systemNote';
export const COMMENT = 'comment';
export const OPENED = 'opened';
export const REOPENED = 'reopened';
export const CLOSED = 'closed';
\ No newline at end of file
export const CLOSED = 'closed';
Loading
Loading
@@ -138,8 +138,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
export const poll = ({ commit, state, getters }) => {
const { notesPath } = $('.js-notes-wrapper')[0].dataset;
 
return service
.poll(`${notesPath}?full_data=1`, state.lastFetchedAt)
return service.poll(`${notesPath}?full_data=1`, state.lastFetchedAt)
.then(res => res.json())
.then((res) => {
if (res.notes.length) {
Loading
Loading
@@ -188,8 +187,8 @@ export const toggleAward = ({ commit, getters, dispatch }, data) => {
});
 
if (amIAwarded) {
Object.assign(data, { awardName: counterAward });
Object.assign(data, { skipMutalityCheck: true });
data.awardName = counterAward;
data.skipMutalityCheck = true;
 
dispatch(types.TOGGLE_AWARD, data);
}
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment