Commit 16a5808e authored by Fatih Acet's avatar Fatih Acet

Implement initial version of Vue notes for issues. 🎉 🎉

parent 0273b118
<script>
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 IssueNoteEditedText from './issue_note_edited_text.vue';
export default {
props: {
note: {
type: Object,
required: true,
},
},
data() {
return {
registerLink: '#',
signInLink: '#',
}
},
computed: {
discussion() {
return this.note.notes[0];
},
author() {
return this.discussion.author;
},
},
components: {
IssueNote,
UserAvatarLink,
IssueNoteHeader,
IssueNoteActions,
IssueNoteEditedText,
},
mounted() {
// We need to grab the register and sign in links from DOM for the time being.
const registerLink = document.querySelector('.js-disabled-comment .js-register-link');
const signInLink = document.querySelector('.js-disabled-comment .js-sign-in-link');
if (registerLink && signInLink) {
this.registerLink = registerLink.getAttribute('href');
this.signInLink = signInLink.getAttribute('href');
}
},
}
</script>
<template>
<li class="note note-discussion timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40" />
</div>
<div class="timeline-content">
<div class="discussion">
<div class="discussion-header">
<issue-note-header
:author="author"
:createdAt="discussion.created_at"
:notePath="discussion.path"
:includeToggle="true"
:discussionId="note.id"
actionText="started a discussion" />
<issue-note-edited-text
v-if="note.last_updated_by"
:editedAt="note.last_updated_at"
:editedBy="note.last_updated_by"
actionText="Last updated"
className="discussion-headline-light js-discussion-headline" />
</div>
</div>
<div
v-if="note.expanded"
class="discussion-body">
<div class="panel panel-default">
<div class="discussion-notes">
<ul class="notes">
<issue-note
v-for="note in note.notes"
key="note.id"
:note="note" />
</ul>
<div class="flash-container"></div>
<div class="discussion-reply-holder">
<button
v-if="note.can_reply"
type="button"
class="btn btn-text-field js-discussion-reply-button"
title="Add a reply"></button>
<div
v-if="!note.can_reply"
class="disabled-comment text-center">
Please
<a :href="registerLink">register</a>
or
<a :href="signInLink">sign in</a>
to reply
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</li>
</template>
<script>
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 IssueNoteEditedText from './issue_note_edited_text.vue';
export default {
props: {
note: {
type: Object,
required: true,
},
},
components: {
UserAvatarLink,
IssueNoteHeader,
IssueNoteActions,
IssueNoteBody,
IssueNoteEditedText,
},
computed: {
author() {
return this.note.author;
},
},
};
</script>
<template>
<li class="note timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
:link-href="author.path"
:img-src="author.avatar_url"
:img-alt="author.name"
:img-size="40" />
</div>
<div class="timeline-content">
<div class="note-header">
<issue-note-header
:author="author"
:createdAt="note.created_at"
:notePath="note.path"
actionText="commented" />
<issue-note-actions
:accessLevel="note.human_access"
:canAward="note.emoji_awardable"
:canEdit="note.can_edit"
:canDelete="note.can_edit"
:reportAbusePath="note.report_abuse_path" />
</div>
<issue-note-body :note="note" />
<issue-note-edited-text
v-if="note.last_edited_by"
:editedAt="note.last_edited_at"
:editedBy="note.last_edited_by"
actionText="Edited" />
</div>
</div>
</li>
</template>
<script>
import emojiSmiling from '../icons/emoji_slightly_smiling_face.svg';
import emojiSmile from '../icons/emoji_smile.svg';
import emojiSmiley from '../icons/emoji_smiley.svg';
export default {
props: {
accessLevel: {
type: String,
required: true,
},
reportAbusePath: {
type: String,
required: true,
},
canEdit: {
type: Boolean,
required: true,
},
canDelete: {
type: Boolean,
required: true,
},
},
data() {
return {
emojiSmiling,
emojiSmile,
emojiSmiley,
};
},
};
</script>
<template>
<div class="note-actions">
<span class="note-role">
{{accessLevel}}
</span>
<a
class="note-action-button note-emoji-button js-add-award js-note-emoji js-user-authored has-tooltip" data-position="right"
href="#"
title="Add reaction">
<i
aria-hidden="true"
data-hidden="true"
class="fa fa-spinner fa-spin"></i>
<span
v-html="emojiSmiling"
class="link-highlight award-control-icon-neutral"></span>
<span
v-html="emojiSmiley"
class="link-highlight award-control-icon-positive"></span>
<span
v-html="emojiSmile"
class="link-highlight award-control-icon-super-positive"></span>
</a>
<div class="dropdown more-actions">
<button
type="button"
title="More actions"
class="note-action-button more-actions-toggle has-tooltip btn btn-transparent"
data-toggle="dropdown"
data-container="body">
<i
aria-hidden="true"
class="fa fa-ellipsis-v icon"></i>
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<template v-if="canEdit">
<li>
<button
type="button"
class="js-note-edit btn btn-transparent">
Edit comment
</button>
</li>
<li class="divider"></li>
</template>
<li v-if="reportAbusePath">
<a :href="reportAbusePath">
Report as abuse
</a>
</li>
<li>
<a class="js-note-delete">
<span class="text-danger">
Delete comment
</span>
</a>
</li>
</ul>
</div>
</div>
</template>
<script>
export default {
props: {
note: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div class="note-body">
<div
v-html="note.note_html"
class="note-text md"></div>
</div>
</template>
<script>
import TimeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
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',
},
},
components: {
TimeAgoTooltip,
},
}
</script>
<template>
<div :class="className">
<span>{{actionText}} </span>
<span> by </span>
<a
:href="editedBy.path"
class="author_link">
<span>{{editedBy.name}}</span>
</a>
<time-ago-tooltip
:time="editedAt"
tooltipPlacement="bottom" />
</div>
</template>
<script>
import TimeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
props: {
author: {
type: Object,
required: true,
},
createdAt: {
type: String,
required: true,
},
actionText: {
type: String,
required: true,
},
notePath: {
type: String,
required: true,
},
includeToggle: {
type: Boolean,
required: false,
default: false,
},
discussionId: {
type: String,
required: false,
}
},
components: {
TimeAgoTooltip,
},
methods: {
doShit() {
this.$store.commit('toggleDiscussion', {
discussionId: this.discussionId,
});
},
},
};
</script>
<template>
<div class="note-header-info">
<a :href="author.path">
<span class="note-header-author-name">
{{author.name}}
</span>
<span class="note-headline-light">
@{{author.username}}
</span>
</a>
<span class="note-headline-light">
<span class="note-headline-meta">
{{actionText}}
<a :href="notePath">
<time-ago-tooltip
:time="createdAt"
tooltipPlacement="bottom" />
</a>
</span>
</span>
<div
v-if="includeToggle"
class="discussion-actions">
<button
@click="doShit"
class="note-action-button discussion-toggle-button js-toggle-button"
type="button">
<i
aria-hidden="true"
class="fa fa-chevron-up"></i>
Toggle discussion
</button>
</div>
</div>
</template>
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import storeOptions from '../stores/issue_notes_store'; import storeOptions from '../stores/issue_notes_store';
import IssueNote from './issue_note.vue';
import IssueDiscussion from './issue_discussion.vue';
Vue.use(Vuex); Vue.use(Vuex);
const store = new Vuex.Store(storeOptions); const store = new Vuex.Store(storeOptions);
...@@ -14,6 +16,18 @@ export default { ...@@ -14,6 +16,18 @@ export default {
isLoading: true, isLoading: true,
}; };
}, },
components: {
IssueNote,
IssueDiscussion,
},
methods: {
component(note) {
return note.individual_note ? IssueNote : IssueDiscussion;
},
componentData(note) {
return note.individual_note ? note.notes[0] : note;
}
},
mounted() { mounted() {
const path = this.$el.parentNode.dataset.discussionsPath; const path = this.$el.parentNode.dataset.discussionsPath;
this.$store.dispatch('fetchNotes', path) this.$store.dispatch('fetchNotes', path)
...@@ -31,7 +45,16 @@ export default { ...@@ -31,7 +45,16 @@ export default {
class="loading"> class="loading">
<i <i
aria-hidden="true" aria-hidden="true"
class="fa fa-spinner fa-spin" /> class="fa fa-spinner fa-spin"></i>
</div> </div>
<ul
class="notes main-notes-list timeline"
id="notes-list">
<component
v-for="note in $store.getters.notes"
:is="component(note)"
:note="componentData(note)"
:key="note.id" />
</ul>
</div> </div>
</template> </template>
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445c.195.625.556 1.131 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg>
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg>
<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="nonzero"/></svg>
...@@ -7,11 +7,20 @@ const state = { ...@@ -7,11 +7,20 @@ const state = {
notes: [], notes: [],
}; };
const getters = {}; const getters = {
notes(storeState) {
return storeState.notes;
},
};
const mutations = { const mutations = {
setNotes(vmState, notes) { setNotes(storeState, notes) {
vmState.notes = notes; storeState.notes = notes;
},
toggleDiscussion(storeState, { discussionId }) {
const [ discussion ] = storeState.notes.filter((note) => note.id === discussionId);
discussion.expanded = !discussion.expanded;
}, },
}; };
...@@ -24,7 +33,7 @@ const actions = { ...@@ -24,7 +33,7 @@ const actions = {
context.commit('setNotes', res); context.commit('setNotes', res);
}) })
.catch(() => { .catch(() => {
new Flash('Something went while fetching issue comments. Please try again.'); // eslint-disable-line new Flash('Something went wrong while fetching issue comments. Please try again.'); // eslint-disable-line
}); });
}, },
}; };
......
...@@ -453,6 +453,7 @@ ...@@ -453,6 +453,7 @@
color: $gray-darkest; color: $gray-darkest;
display: block; display: block;
margin: 16px 0 0; margin: 16px 0 0;
font-size: 85%;
.author_link { .author_link {
color: $gray-darkest; color: $gray-darkest;
......
...@@ -15,11 +15,11 @@ ...@@ -15,11 +15,11 @@
.timeline-content.timeline-content-form .timeline-content.timeline-content-form
= render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete = render "shared/notes/form", view: diff_view, supports_autocomplete: autocomplete
- elsif !current_user - elsif !current_user
.disabled-comment.text-center.prepend-top-default .disabled-comment.text-center.prepend-top-default.js-disabled-comment
Please Please
= link_to "register", new_session_path(:user, redirect_to_referer: 'yes') = link_to "register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-register-link'
or or
= link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes') = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link'
to comment to comment
:javascript :javascript
......
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