Commit 88bd9bac authored by Mike Greiling's avatar Mike Greiling

Merge branch 'acet-notes-prettified' into 'master'

Prettify notes app

See merge request gitlab-org/gitlab-ce!17812
parents bcc04515 fdc9ae2e
...@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue'; ...@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores'; import store from '../notes/stores';
export default function initMrNotes() { export default function initMrNotes() {
new Vue({ // eslint-disable-line // eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions', el: '#js-vue-mr-discussions',
components: { components: {
notesApp, notesApp,
}, },
data() { data() {
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
...@@ -28,7 +30,8 @@ export default function initMrNotes() { ...@@ -28,7 +30,8 @@ export default function initMrNotes() {
}, },
}); });
new Vue({ // eslint-disable-line // eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter', el: '#js-vue-discussion-counter',
components: { components: {
discussionCounter, discussionCounter,
......
This diff is collapsed.
<script> <script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
ClipboardButton, ClipboardButton,
Icon, Icon,
},
props: {
diffFile: {
type: Object,
required: true,
}, },
props: { },
diffFile: { computed: {
type: Object, titleTag() {
required: true, return this.diffFile.discussionPath ? 'a' : 'span';
},
}, },
computed: { },
titleTag() { };
return this.diffFile.discussionPath ? 'a' : 'span';
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight'; import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index'; import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
export default { export default {
components: { components: {
DiffFileHeader, DiffFileHeader,
},
props: {
discussion: {
type: Object,
required: true,
}, },
props: { },
discussion: { computed: {
type: Object, isImageDiff() {
required: true, return !this.diffFile.text;
},
}, },
computed: { diffFileClass() {
isImageDiff() { const { text } = this.diffFile;
return !this.diffFile.text; return text ? 'text-file' : 'js-image-file';
},
diffFileClass() {
const { text } = this.diffFile;
return text ? 'text-file' : 'js-image-file';
},
diffRows() {
return $(this.discussion.truncatedDiffLines);
},
diffFile() {
return convertObjectPropsToCamelCase(this.discussion.diffFile);
},
imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
}, },
mounted() { diffRows() {
if (this.isImageDiff) { return $(this.discussion.truncatedDiffLines);
const canCreateNote = false;
const renderCommentBadge = true;
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
} else {
const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => {
syntaxHighlight(fileHolder);
});
}
}, },
methods: { diffFile() {
rowTag(html) { return convertObjectPropsToCamelCase(this.discussion.diffFile);
return html.outerHTML ? 'tr' : 'template';
},
}, },
}; imageDiffHtml() {
return this.discussion.imageDiffHtml;
},
},
mounted() {
if (this.isImageDiff) {
const canCreateNote = false;
const renderCommentBadge = true;
imageDiffHelper.initImageDiff(
this.$refs.fileHolder,
canCreateNote,
renderCommentBadge,
);
} else {
const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => {
syntaxHighlight(fileHolder);
});
}
},
methods: {
rowTag(html) {
return html.outerHTML ? 'tr' : 'template';
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg'; import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg'; import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg'; import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg'; import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils'; import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
computed: {
...mapGetters([
'getUserData',
'getNoteableData',
'discussionCount',
'unresolvedDiscussions',
'resolvedDiscussionCount',
]),
isLoggedIn() {
return this.getUserData.id;
}, },
computed: { hasNextButton() {
...mapGetters([ return this.isLoggedIn && !this.allResolved;
'getUserData', },
'getNoteableData', countText() {
'discussionCount', return pluralize('discussion', this.discussionCount);
'unresolvedDiscussions', },
'resolvedDiscussionCount', allResolved() {
]), return this.resolvedDiscussionCount === this.discussionCount;
isLoggedIn() {
return this.getUserData.id;
},
hasNextButton() {
return this.isLoggedIn && !this.allResolved;
},
countText() {
return pluralize('discussion', this.discussionCount);
},
allResolved() {
return this.resolvedDiscussionCount === this.discussionCount;
},
resolveAllDiscussionsIssuePath() {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
firstUnresolvedDiscussionId() {
const item = this.unresolvedDiscussions[0] || {};
return item.id;
},
}, },
created() { resolveAllDiscussionsIssuePath() {
this.resolveSvg = resolveSvg; return this.getNoteableData.create_issue_to_resolve_discussions_path;
this.resolvedSvg = resolvedSvg; },
this.mrIssueSvg = mrIssueSvg; firstUnresolvedDiscussionId() {
this.nextDiscussionSvg = nextDiscussionSvg; const item = this.unresolvedDiscussions[0] || {};
return item.id;
}, },
methods: { },
jumpToFirstDiscussion() { created() {
const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`); this.resolveSvg = resolveSvg;
const activeTab = window.mrTabs.currentAction; this.resolvedSvg = resolvedSvg;
this.mrIssueSvg = mrIssueSvg;
this.nextDiscussionSvg = nextDiscussionSvg;
},
methods: {
jumpToFirstDiscussion() {
const el = document.querySelector(
`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') { if (activeTab === 'commits' || activeTab === 'pipelines') {
window.mrTabs.activateTab('show'); window.mrTabs.activateTab('show');
} }
if (el) { if (el) {
scrollToElement(el); scrollToElement(el);
} }
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Issuable from '~/vue_shared/mixins/issuable'; import Issuable from '~/vue_shared/mixins/issuable';
export default { export default {
components: { components: {
Icon, Icon,
}, },
mixins: [ mixins: [Issuable],
Issuable, };
],
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg'; import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'NoteActions', name: 'NoteActions',
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
loadingIcon, loadingIcon,
}, },
props: { props: {
authorId: { authorId: {
type: Number, type: Number,
required: true, required: true,
}, },
noteId: { noteId: {
type: Number, type: Number,
required: true, required: true,
}, },
accessLevel: { accessLevel: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
reportAbusePath: { reportAbusePath: {
type: String, type: String,
required: true, required: true,
}, },
canEdit: { canEdit: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
canDelete: { canDelete: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
resolvable: { resolvable: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
isResolved: { isResolved: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
isResolving: { isResolving: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
resolvedBy: { resolvedBy: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}), default: () => ({}),
}, },
canReportAsAbuse: { canReportAsAbuse: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['getUserDataByProp']),
'getUserDataByProp', shouldShowActionsDropdown() {
]), return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
shouldShowActionsDropdown() { },
return this.currentUserId && (this.canEdit || this.canReportAsAbuse); canAddAwardEmoji() {
}, return this.currentUserId;
canAddAwardEmoji() { },
return this.currentUserId; isAuthoredByCurrentUser() {
}, return this.authorId === this.currentUserId;
isAuthoredByCurrentUser() { },
return this.authorId === this.currentUserId; currentUserId() {
}, return this.getUserDataByProp('id');
currentUserId() { },
return this.getUserDataByProp('id'); resolveButtonTitle() {
}, let title = 'Mark as resolved';
resolveButtonTitle() {
let title = 'Mark as resolved';
if (this.resolvedBy) { if (this.resolvedBy) {
title = `Resolved by ${this.resolvedBy.name}`; title = `Resolved by ${this.resolvedBy.name}`;
} }
return title; return title;
}, },
}, },
created() { created() {
this.emojiSmiling = emojiSmiling; this.emojiSmiling = emojiSmiling;
this.emojiSmile = emojiSmile; this.emojiSmile = emojiSmile;
this.emojiSmiley = emojiSmiley; this.emojiSmiley = emojiSmiley;
this.editSvg = editSvg; this.editSvg = editSvg;
this.ellipsisSvg = ellipsisSvg; this.ellipsisSvg = ellipsisSvg;
this.resolveDiscussionSvg = resolveDiscussionSvg; this.resolveDiscussionSvg = resolveDiscussionSvg;
this.resolvedDiscussionSvg = resolvedDiscussionSvg; this.resolvedDiscussionSvg = resolvedDiscussionSvg;
}, },
methods: { methods: {
onEdit() { onEdit() {
this.$emit('handleEdit'); this.$emit('handleEdit');
}, },
onDelete() { onDelete() {
this.$emit('handleDelete'); this.$emit('handleDelete');
}, },
onResolve() { onResolve() {
this.$emit('handleResolve'); this.$emit('handleResolve');
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
export default { export default {
name: 'NoteAttachment', name: 'NoteAttachment',
props: { props: {
attachment: { attachment: {
type: Object, type: Object,
required: true, required: true,
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import noteEditedText from './note_edited_text.vue'; import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue'; import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue'; import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue'; import noteForm from './note_form.vue';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
components: { components: {
noteEditedText, noteEditedText,
noteAwardsList, noteAwardsList,
noteAttachment, noteAttachment,
noteForm, noteForm,
},
mixins: [autosave],
props: {
note: {
type: Object,
required: true,
}, },
mixins: [ canEdit: {
autosave, type: Boolean,
], required: true,
props: {
note: {
type: Object,
required: true,
},
canEdit: {
type: Boolean,
required: true,
},
isEditing: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { isEditing: {
noteBody() { type: Boolean,
return this.note.note; required: false,
}, default: false,
}, },
mounted() { },
this.renderGFM(); computed: {
this.initTaskList(); noteBody() {
return this.note.note;
},
},
mounted() {
this.renderGFM();
this.initTaskList();
if (this.isEditing) {
this.initAutoSave(this.note.noteable_type);
}
},
updated() {
this.initTaskList();
this.renderGFM();
if (this.isEditing) { if (this.isEditing) {
if (!this.autosave) {
this.initAutoSave(this.note.noteable_type); this.initAutoSave(this.note.noteable_type);
} else {
this.setAutoSave();
} }
}
},
methods: {
renderGFM() {
$(this.$refs['note-body']).renderGFM();
}, },
updated() { initTaskList() {
this.initTaskList(); if (this.canEdit) {
this.renderGFM(); this.taskList = new TaskList({
dataType: 'note',
if (this.isEditing) { fieldName: 'note',
if (!this.autosave) { selector: '.notes',
this.initAutoSave(this.note.noteable_type); });
} else {
this.setAutoSave();
}
} }
}, },
methods: { handleFormUpdate(note, parentElement, callback) {
renderGFM() { this.$emit('handleFormUpdate', note, parentElement, callback);
$(this.$refs['note-body']).renderGFM(); },
}, formCancelHandler(shouldConfirm, isDirty) {
initTaskList() { this.$emit('cancelFormEdition', shouldConfirm, isDirty);
if (this.canEdit) {
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes',
});
}
},
handleFormUpdate(note, parentElement, callback) {
this.$emit('handleFormUpdate', note, parentElement, callback);
},
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelFormEdition', shouldConfirm, isDirty);
},
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
name: 'EditedNoteText', name: 'EditedNoteText',
components: { components: {
timeAgoTooltip, timeAgoTooltip,
},
props: {
actionText: {
type: String,
required: true,
}, },
props: { editedAt: {
actionText: { type: String,
type: String, required: true,
required: true,
},
editedAt: {
type: String,
required: true,
},
editedBy: {
type: Object,
required: false,
default: () => ({}),
},
className: {
type: String,
required: false,
default: 'edited-text',
},
}, },
}; editedBy: {
type: Object,
required: false,
default: () => ({}),
},
className: {
type: String,
required: false,
default: 'edited-text',
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
name: 'IssueNoteForm', name: 'IssueNoteForm',
components: { components: {
issueWarning, issueWarning,
markdownField, markdownField,
},
mixins: [issuableStateMixin, resolvable],
props: {
noteBody: {
type: String,
required: false,
default: '',
}, },
mixins: [ noteId: {
issuableStateMixin, type: Number,
resolvable, required: false,
], default: 0,
props: {
noteBody: {
type: String,
required: false,
default: '',
},
noteId: {
type: Number,
required: false,
default: 0,
},
saveButtonTitle: {
type: String,
required: false,
default: 'Save comment',
},
note: {
type: Object,
required: false,
default: () => ({}),
},
isEditing: {
type: Boolean,
required: true,
},
}, },
data() { saveButtonTitle: {
return { type: String,
updatedNoteBody: this.noteBody, required: false,
conflictWhileEditing: false, default: 'Save comment',
isSubmitting: false,
isResolving: false,
resolveAsThread: true,
};
}, },
computed: { note: {
...mapGetters([ type: Object,
'getDiscussionLastNote', required: false,
'getNoteableData', default: () => ({}),
'getNoteableDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
]),
noteHash() {
return `#note_${this.noteId}`;
},
markdownPreviewPath() {
return this.getNoteableDataByProp('preview_note_path');
},
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
},
currentUserId() {
return this.getUserDataByProp('id');
},
isDisabled() {
return !this.updatedNoteBody.length || this.isSubmitting;
},
}, },
watch: { isEditing: {
noteBody() { type: Boolean,
if (this.updatedNoteBody === this.noteBody) { required: true,
this.updatedNoteBody = this.noteBody; },
} else { },
this.conflictWhileEditing = true; data() {
} return {
}, updatedNoteBody: this.noteBody,
conflictWhileEditing: false,
isSubmitting: false,
isResolving: false,
resolveAsThread: true,
};
},
computed: {
...mapGetters([
'getDiscussionLastNote',
'getNoteableData',
'getNoteableDataByProp',
'getNotesDataByProp',
'getUserDataByProp',
]),
noteHash() {
return `#note_${this.noteId}`;
},
markdownPreviewPath() {
return this.getNoteableDataByProp('preview_note_path');
},
markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
return !this.isEditing
? this.getNotesDataByProp('quickActionsDocsPath')
: undefined;
}, },
mounted() { currentUserId() {
this.$refs.textarea.focus(); return this.getUserDataByProp('id');
}, },
methods: { isDisabled() {
...mapActions([ return !this.updatedNoteBody.length || this.isSubmitting;
'toggleResolveNote', },
]), },
handleUpdate(shouldResolve) { watch: {
const beforeSubmitDiscussionState = this.discussionResolved; noteBody() {
this.isSubmitting = true; if (this.updatedNoteBody === this.noteBody) {
this.updatedNoteBody = this.noteBody;
} else {
this.conflictWhileEditing = true;
}
},
},
mounted() {
this.$refs.textarea.focus();
},
methods: {
...mapActions(['toggleResolveNote']),
handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { this.$emit(
'handleFormUpdate',
this.updatedNoteBody,
this.$refs.editNoteForm,
() => {
this.isSubmitting = false; this.isSubmitting = false;
if (shouldResolve) { if (shouldResolve) {
this.resolveHandler(beforeSubmitDiscussionState); this.resolveHandler(beforeSubmitDiscussionState);
} }
}); },
}, );
editMyLastNote() { },
if (this.updatedNoteBody === '') { editMyLastNote() {
const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody); if (this.updatedNoteBody === '') {
const lastNoteInDiscussion = this.getDiscussionLastNote(
this.updatedNoteBody,
);
if (lastNoteInDiscussion) { if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', { eventHub.$emit('enterEditMode', {
noteId: lastNoteInDiscussion.id, noteId: lastNoteInDiscussion.id,
}); });
}
} }
}, }
cancelHandler(shouldConfirm = false) { },
// Sends information about confirm message and if the textarea has changed cancelHandler(shouldConfirm = false) {
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody); // Sends information about confirm message and if the textarea has changed
}, this.$emit(
'cancelFormEdition',
shouldConfirm,
this.noteBody !== this.updatedNoteBody,
);
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
components: { components: {
timeAgoTooltip, timeAgoTooltip,
},
props: {
author: {
type: Object,
required: true,
}, },
props: { createdAt: {
author: { type: String,
type: Object, required: true,
required: true,
},
createdAt: {
type: String,
required: true,
},
actionText: {
type: String,
required: false,
default: '',
},
actionTextHtml: {
type: String,
required: false,
default: '',
},
noteId: {
type: Number,
required: true,
},
includeToggle: {
type: Boolean,
required: false,
default: false,
},
expanded: {
type: Boolean,
required: false,
default: true,
},
}, },
computed: { actionText: {
toggleChevronClass() { type: String,
return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down'; required: false,
}, default: '',
noteTimestampLink() {
return `#note_${this.noteId}`;
},
}, },
methods: { actionTextHtml: {
...mapActions([ type: String,
'setTargetNoteHash', required: false,
]), default: '',
handleToggle() {
this.$emit('toggleHandler');
},
updateTargetNoteHash() {
this.setTargetNoteHash(this.noteTimestampLink);
},
}, },
}; noteId: {
type: Number,
required: true,
},
includeToggle: {
type: Boolean,
required: false,
default: false,
},
expanded: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
toggleChevronClass() {
return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
},
noteTimestampLink() {
return `#note_${this.noteId}`;
},
},
methods: {
...mapActions(['setTargetNoteHash']),
handleToggle() {
this.$emit('toggleHandler');
},
updateTargetNoteHash() {
this.setTargetNoteHash(this.noteTimestampLink);
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
computed: { computed: {
...mapGetters([ ...mapGetters(['getNotesDataByProp']),
'getNotesDataByProp', registerLink() {
]), return this.getNotesDataByProp('registerPath');
registerLink() {
return this.getNotesDataByProp('registerPath');
},
signInLink() {
return this.getNotesDataByProp('newSessionPath');
},
}, },
}; signInLink() {
return this.getNotesDataByProp('newSessionPath');
},
},
};
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore'; import { escape } from 'underscore';
import Flash from '../../flash'; import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue'; import noteActions from './note_actions.vue';
import noteBody from './note_body.vue'; import noteBody from './note_body.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
components: { components: {
userAvatarLink, userAvatarLink,
noteHeader, noteHeader,
noteActions, noteActions,
noteBody, noteBody,
},
mixins: [noteable, resolvable],
props: {
note: {
type: Object,
required: true,
}, },
mixins: [ },
noteable, data() {
resolvable, return {
], isEditing: false,
props: { isDeleting: false,
note: { isRequesting: false,
type: Object, isResolving: false,
required: true, };
}, },
computed: {
...mapGetters(['targetNoteHash', 'getUserData']),
author() {
return this.note.author;
}, },
data() { classNameBindings() {
return { return {
isEditing: false, 'is-editing': this.isEditing && !this.isRequesting,
isDeleting: false, 'is-requesting being-posted': this.isRequesting,
isRequesting: false, 'disabled-content': this.isDeleting,
isResolving: false, target: this.targetNoteHash === this.noteAnchorId,
}; };
}, },
computed: { canReportAsAbuse() {
...mapGetters([ return (
'targetNoteHash', this.note.report_abuse_path && this.author.id !== this.getUserData.id
'getUserData', );
]),
author() {
return this.note.author;
},
classNameBindings() {
return {
'is-editing': this.isEditing && !this.isRequesting,
'is-requesting being-posted': this.isRequesting,
'disabled-content': this.isDeleting,
target: this.targetNoteHash === this.noteAnchorId,
};
},
canReportAsAbuse() {
return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
noteAnchorId() {
return `note_${this.note.id}`;
},
}, },
noteAnchorId() {
created() { return `note_${this.note.id}`;
eventHub.$on('enterEditMode', ({ noteId }) => {
if (noteId === this.note.id) {
this.isEditing = true;
this.scrollToNoteIfNeeded($(this.$el));
}
});
}, },
},
methods: { created() {
...mapActions([ eventHub.$on('enterEditMode', ({ noteId }) => {
'deleteNote', if (noteId === this.note.id) {
'updateNote',
'toggleResolveNote',
'scrollToNoteIfNeeded',
]),
editHandler() {
this.isEditing = true; this.isEditing = true;
}, this.scrollToNoteIfNeeded($(this.$el));
deleteHandler() { }
// eslint-disable-next-line no-alert });
if (confirm('Are you sure you want to delete this comment?')) { },
this.isDeleting = true;
this.deleteNote(this.note) methods: {
.then(() => { ...mapActions([
this.isDeleting = false; 'deleteNote',
}) 'updateNote',
.catch(() => { 'toggleResolveNote',
Flash('Something went wrong while deleting your note. Please try again.'); 'scrollToNoteIfNeeded',
this.isDeleting = false; ]),
}); editHandler() {
} this.isEditing = true;
}, },
formUpdateHandler(noteText, parentElement, callback) { deleteHandler() {
const data = { // eslint-disable-next-line no-alert
endpoint: this.note.path, if (confirm('Are you sure you want to delete this comment?')) {
note: { this.isDeleting = true;
target_type: this.noteableType,
target_id: this.note.noteable_id,
note: { note: noteText },
},
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
this.note.note_html = escape(noteText);
this.updateNote(data) this.deleteNote(this.note)
.then(() => { .then(() => {
this.isEditing = false; this.isDeleting = false;
this.isRequesting = false;
this.oldContent = null;
$(this.$refs.noteBody.$el).renderGFM();
this.$refs.noteBody.resetAutoSave();
callback();
}) })
.catch(() => { .catch(() => {
this.isRequesting = false; Flash(
this.isEditing = true; 'Something went wrong while deleting your note. Please try again.',
this.$nextTick(() => { );
const msg = 'Something went wrong while editing your comment. Please try again.'; this.isDeleting = false;
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
}); });
}, }
formCancelHandler(shouldConfirm, isDirty) { },
if (shouldConfirm && isDirty) { formUpdateHandler(noteText, parentElement, callback) {
// eslint-disable-next-line no-alert const data = {
if (!confirm('Are you sure you want to cancel editing this comment?')) return; endpoint: this.note.path,
} note: {
this.$refs.noteBody.resetAutoSave(); target_type: this.noteableType,
if (this.oldContent) { target_id: this.note.noteable_id,
this.note.note_html = this.oldContent; note: { note: noteText },
},
};
this.isRequesting = true;
this.oldContent = this.note.note_html;
this.note.note_html = escape(noteText);
this.updateNote(data)
.then(() => {
this.isEditing = false;
this.isRequesting = false;
this.oldContent = null; this.oldContent = null;
} $(this.$refs.noteBody.$el).renderGFM();
this.isEditing = false; this.$refs.noteBody.resetAutoSave();
}, callback();
recoverNoteContent(noteText) { })
// we need to do this to prevent noteForm inconsistent content warning .catch(() => {
// this is something we intentionally do so we need to recover the content this.isRequesting = false;
this.note.note = noteText; this.isEditing = true;
this.$refs.noteBody.$refs.noteForm.note.note = noteText; this.$nextTick(() => {
}, const msg =
'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
});
});
},
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel editing this comment?'))
return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
this.note.note_html = this.oldContent;
this.oldContent = null;
}
this.isEditing = false;
},
recoverNoteContent(noteText) {
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
this.$refs.noteBody.$refs.noteForm.note.note = noteText;
}, },
}; },
};
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility'; import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash'; import Flash from '../../flash';
import store from '../stores/'; import store from '../stores/';
import * as constants from '../constants'; import * as constants from '../constants';
import noteableNote from './noteable_note.vue'; import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue'; import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue'; import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default { export default {
name: 'NotesApp', name: 'NotesApp',
components: { components: {
noteableNote, noteableNote,
noteableDiscussion, noteableDiscussion,
systemNote, systemNote,
commentForm, commentForm,
loadingIcon, loadingIcon,
placeholderNote, placeholderNote,
placeholderSystemNote, placeholderSystemNote,
},
props: {
noteableData: {
type: Object,
required: true,
}, },
props: { notesData: {
noteableData: { type: Object,
type: Object, required: true,
required: true,
},
notesData: {
type: Object,
required: true,
},
userData: {
type: Object,
required: false,
default: () => ({}),
},
}, },
store, userData: {
data() { type: Object,
return { required: false,
isLoading: true, default: () => ({}),
};
}, },
computed: { },
...mapGetters([ store,
'notes', data() {
'getNotesDataByProp', return {
'discussionCount', isLoading: true,
]), };
noteableType() { },
// FIXME -- @fatihacet Get this from JSON data. computed: {
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
// FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE; return this.noteableData.merge_params
}, ? MERGE_REQUEST_NOTEABLE_TYPE
allNotes() { : ISSUE_NOTEABLE_TYPE;
if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
return new Array(totalNotes).fill({
isSkeletonNote: true,
});
}
return this.notes;
},
},
created() {
this.setNotesData(this.notesData);
this.setNoteableData(this.noteableData);
this.setUserData(this.userData);
}, },
mounted() { allNotes() {
this.fetchNotes(); if (this.isLoading) {
const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
const parentElement = this.$el.parentElement; return new Array(totalNotes).fill({
isSkeletonNote: true,
if (parentElement &&
parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', (event) => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
}); });
} }
document.addEventListener('refreshVueNotes', this.fetchNotes); return this.notes;
},
beforeDestroy() {
document.removeEventListener('refreshVueNotes', this.fetchNotes);
}, },
methods: { },
...mapActions({ created() {
actionFetchNotes: 'fetchNotes', this.setNotesData(this.notesData);
poll: 'poll', this.setNoteableData(this.noteableData);
actionToggleAward: 'toggleAward', this.setUserData(this.userData);
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', },
setNotesData: 'setNotesData', mounted() {
setNoteableData: 'setNoteableData', this.fetchNotes();
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt', const parentElement = this.$el.parentElement;
setTargetNoteHash: 'setTargetNoteHash',
}),
getComponentName(note) {
if (note.isSkeletonNote) {
return skeletonLoadingContainer;
}
if (note.isPlaceholderNote) {
if (note.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
}
return placeholderNote;
} else if (note.individual_note) {
return note.notes[0].system ? systemNote : noteableNote;
}
return noteableDiscussion; if (
}, parentElement &&
getComponentData(note) { parentElement.classList.contains('js-vue-notes-event')
return note.individual_note ? note.notes[0] : note; ) {
}, parentElement.addEventListener('toggleAward', event => {
fetchNotes() { const { awardName, noteId } = event.detail;
return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath')) this.actionToggleAward({ awardName, noteId });
.then(() => this.initPolling()) });
.then(() => { }
this.isLoading = false; document.addEventListener('refreshVueNotes', this.fetchNotes);
}) },
.then(() => this.$nextTick()) beforeDestroy() {
.then(() => this.checkLocationHash()) document.removeEventListener('refreshVueNotes', this.fetchNotes);
.catch(() => { },
this.isLoading = false; methods: {
Flash('Something went wrong while fetching comments. Please try again.'); ...mapActions({
}); actionFetchNotes: 'fetchNotes',
}, poll: 'poll',
initPolling() { actionToggleAward: 'toggleAward',
if (this.isPollingInitialized) { scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
return; setNotesData: 'setNotesData',
setNoteableData: 'setNoteableData',
setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash',
}),
getComponentName(note) {
if (note.isSkeletonNote) {
return skeletonLoadingContainer;
}
if (note.isPlaceholderNote) {
if (note.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
} }
return placeholderNote;
} else if (note.individual_note) {
return note.notes[0].system ? systemNote : noteableNote;
}
this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt')); return noteableDiscussion;
},
getComponentData(note) {
return note.individual_note ? note.notes[0] : note;
},
fetchNotes() {
return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath'))
.then(() => this.initPolling())
.then(() => {
this.isLoading = false;
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
Flash(
'Something went wrong while fetching comments. Please try again.',
);
});
},
initPolling() {
if (this.isPollingInitialized) {
return;
}
this.poll(); this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
this.isPollingInitialized = true;
},
checkLocationHash() {
const hash = getLocationHash();
const element = document.getElementById(hash);
if (hash && element) { this.poll();
this.setTargetNoteHash(hash); this.isPollingInitialized = true;
this.scrollToNoteIfNeeded($(element)); },
} checkLocationHash() {
}, const hash = getLocationHash();
const element = document.getElementById(hash);
if (hash && element) {
this.setTargetNoteHash(hash);
this.scrollToNoteIfNeeded($(element));
}
}, },
}; },
};
</script> </script>
<template> <template>
......
import Vue from 'vue'; import Vue from 'vue';
import notesApp from './components/notes_app.vue'; import notesApp from './components/notes_app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener(
el: '#js-vue-notes', 'DOMContentLoaded',
components: { () =>
notesApp, new Vue({
}, el: '#js-vue-notes',
data() { components: {
const notesDataset = document.getElementById('js-vue-notes').dataset; notesApp,
const parsedUserData = JSON.parse(notesDataset.currentUserData); },
const currentUserData = parsedUserData ? { data() {
id: parsedUserData.id, const notesDataset = document.getElementById('js-vue-notes').dataset;
name: parsedUserData.name, const parsedUserData = JSON.parse(notesDataset.currentUserData);
username: parsedUserData.username, let currentUserData = {};
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path, if (parsedUserData) {
} : {}; currentUserData = {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
};
}
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData, currentUserData,
notesData: JSON.parse(notesDataset.notesData), notesData: JSON.parse(notesDataset.notesData),
}; };
}, },
render(createElement) { render(createElement) {
return createElement('notes-app', { return createElement('notes-app', {
props: { props: {
noteableData: this.noteableData, noteableData: this.noteableData,
notesData: this.notesData, notesData: this.notesData,
userData: this.currentUserData, userData: this.currentUserData,
},
});
}, },
}); }),
}, );
}));
...@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; ...@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default { export default {
methods: { methods: {
initAutoSave(noteableType) { initAutoSave(noteableType) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]); this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note',
capitalizeFirstCharacter(noteableType),
this.note.id,
]);
}, },
resetAutoSave() { resetAutoSave() {
this.autosave.reset(); this.autosave.reset();
......
...@@ -12,7 +12,8 @@ export default { ...@@ -12,7 +12,8 @@ export default {
discussionResolved() { discussionResolved() {
const { notes, resolved } = this.note; const { notes, resolved } = this.note;
if (notes) { // Decide resolved state using store. Only valid for discussions. if (notes) {
// Decide resolved state using store. Only valid for discussions.
return notes.every(note => note.resolved && !note.system); return notes.every(note => note.resolved && !note.system);
} }
...@@ -26,7 +27,9 @@ export default { ...@@ -26,7 +27,9 @@ export default {
return __('Comment and resolve discussion'); return __('Comment and resolve discussion');
} }
return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion'); return this.discussionResolved
? __('Unresolve discussion')
: __('Resolve discussion');
}, },
}, },
methods: { methods: {
...@@ -42,7 +45,9 @@ export default { ...@@ -42,7 +45,9 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isResolving = false; this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.'); const msg = __(
'Something went wrong while resolving this discussion. Please try again.',
);
Flash(msg, 'alert', this.$el); Flash(msg, 'alert', this.$el);
}); });
}, },
......
...@@ -22,7 +22,9 @@ export default { ...@@ -22,7 +22,9 @@ export default {
}, },
toggleResolveNote(endpoint, isResolved) { toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants; const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME; const method = isResolved
? UNRESOLVE_NOTE_METHOD_NAME
: RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint); return Vue.http[method](endpoint);
}, },
......
...@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop]; ...@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const openState = state => state.noteableData.state; export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {}; export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const getUserDataByProp = state => prop =>
state.userData && state.userData[prop];
export const notesById = state => state.notes.reduce((acc, note) => { export const notesById = state =>
note.notes.every(n => Object.assign(acc, { [n.id]: n })); state.notes.reduce((acc, note) => {
return acc; note.notes.every(n => Object.assign(acc, { [n.id]: n }));
}, {}); return acc;
}, {});
const reverseNotes = array => array.slice(0).reverse(); const reverseNotes = array => array.slice(0).reverse();
const isLastNote = (note, state) => !note.system && const isLastNote = (note, state) =>
state.userData && note.author && !note.system &&
state.userData &&
note.author &&
note.author.id === state.userData.id; note.author.id === state.userData.id;
export const getCurrentUserLastNote = state => _.flatten( export const getCurrentUserLastNote = state =>
reverseNotes(state.notes) _.flatten(
.map(note => reverseNotes(note.notes)), reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
).find(el => isLastNote(el, state)); ).find(el => isLastNote(el, state));
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) export const getDiscussionLastNote = state => discussion =>
.find(el => isLastNote(el, state)); reverseNotes(discussion.notes).find(el => isLastNote(el, state));
export const discussionCount = (state) => { export const discussionCount = state => {
const discussions = state.notes.filter(n => !n.individual_note); const discussions = state.notes.filter(n => !n.individual_note);
return discussions.length; return discussions.length;
...@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => { ...@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]); return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
}; };
export const resolvedDiscussionsById = (state) => { export const resolvedDiscussionsById = state => {
const map = {}; const map = {};
state.notes.forEach((n) => { state.notes.forEach(n => {
if (n.notes) { if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system); const resolved = n.notes.every(note => note.resolved && !note.system);
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
[types.ADD_NEW_NOTE](state, note) { [types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note; const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id); const [exists] = state.notes.filter(n => n.id === note.discussion_id);
const isDiscussion = (type === constants.DISCUSSION_NOTE); const isDiscussion = type === constants.DISCUSSION_NOTE;
if (!exists) { if (!exists) {
const noteData = { const noteData = {
...@@ -63,13 +63,15 @@ export default { ...@@ -63,13 +63,15 @@ export default {
const note = notes[i]; const note = notes[i];
const children = note.notes; const children = note.notes;
if (children.length && !note.individual_note) { // remove placeholder from discussions if (children.length && !note.individual_note) {
// remove placeholder from discussions
for (let j = children.length - 1; j >= 0; j -= 1) { for (let j = children.length - 1; j >= 0; j -= 1) {
if (children[j].isPlaceholderNote) { if (children[j].isPlaceholderNote) {
children.splice(j, 1); children.splice(j, 1);
} }
} }
} else if (note.isPlaceholderNote) { // remove placeholders from state root } else if (note.isPlaceholderNote) {
// remove placeholders from state root
notes.splice(i, 1); notes.splice(i, 1);
} }
} }
...@@ -89,10 +91,10 @@ export default { ...@@ -89,10 +91,10 @@ export default {
[types.SET_INITIAL_NOTES](state, notesData) { [types.SET_INITIAL_NOTES](state, notesData) {
const notes = []; const notes = [];
notesData.forEach((note) => { notesData.forEach(note => {
// To support legacy notes, should be very rare case. // To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) { if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => { note.notes.forEach(n => {
notes.push({ notes.push({
...note, ...note,
notes: [n], // override notes array to only have one item to mimick individual_note notes: [n], // override notes array to only have one item to mimick individual_note
...@@ -103,7 +105,7 @@ export default { ...@@ -103,7 +105,7 @@ export default {
notes.push({ notes.push({
...note, ...note,
expanded: (oldNote ? oldNote.expanded : note.expanded), expanded: oldNote ? oldNote.expanded : note.expanded,
}); });
} }
}); });
...@@ -128,7 +130,9 @@ export default { ...@@ -128,7 +130,9 @@ export default {
notesArr.push({ notesArr.push({
individual_note: true, individual_note: true,
isPlaceholderNote: true, isPlaceholderNote: true,
placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE, placeholderType: data.isSystemNote
? constants.SYSTEM_NOTE
: constants.NOTE,
notes: [ notes: [
{ {
body: data.noteBody, body: data.noteBody,
...@@ -141,12 +145,16 @@ export default { ...@@ -141,12 +145,16 @@ export default {
const { awardName, note } = data; const { awardName, note } = data;
const { id, name, username } = state.userData; const { id, name, username } = state.userData;
const hasEmojiAwardedByCurrentUser = note.award_emoji const hasEmojiAwardedByCurrentUser = note.award_emoji.filter(
.filter(emoji => emoji.name === data.awardName && emoji.user.id === id); emoji => emoji.name === data.awardName && emoji.user.id === id,
);
if (hasEmojiAwardedByCurrentUser.length) { if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it. // If current user has awarded this emoji, remove it.
note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1); note.award_emoji.splice(
note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
1,
);
} else { } else {
note.award_emoji.push({ note.award_emoji.push({
name: awardName, name: awardName,
......
...@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache'; ...@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; export const findNoteObjectById = (notes, id) =>
notes.filter(n => n.id === id)[0];
export const getQuickActionText = (note) => { export const getQuickActionText = note => {
let text = 'Applying command'; let text = 'Applying command';
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; const quickActions =
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter((command) => { const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`); const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(note); return commandRegex.test(note);
}); });
...@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => { ...@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); export const stripQuickActions = note =>
note.replace(REGEX_QUICK_ACTIONS, '').trim();
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment