Commit 4c351e17 authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'ce-1984-frontend-for-batch-comments' into 'master'

Backport CE changes for: [Frontend only] Batch comments on merge requests

See merge request gitlab-org/gitlab-ce!22158
parents 54c442af 8e5c0e68
...@@ -127,7 +127,6 @@ export default { ...@@ -127,7 +127,6 @@ export default {
'startRenderDiffsQueue', 'startRenderDiffsQueue',
'assignDiscussionsToDiff', 'assignDiscussionsToDiff',
]), ]),
fetchData() { fetchData() {
this.fetchDiffFiles() this.fetchDiffFiles()
.then(() => { .then(() => {
......
...@@ -25,7 +25,7 @@ export const getReversePosition = linePosition => { ...@@ -25,7 +25,7 @@ export const getReversePosition = linePosition => {
return LINE_POSITION_RIGHT; return LINE_POSITION_RIGHT;
}; };
export function getNoteFormData(params) { export function getFormData(params) {
const { const {
note, note,
noteableType, noteableType,
...@@ -70,9 +70,15 @@ export function getNoteFormData(params) { ...@@ -70,9 +70,15 @@ export function getNoteFormData(params) {
}, },
}; };
return postData;
}
export function getNoteFormData(params) {
const data = getFormData(params);
return { return {
endpoint: noteableData.create_note_path, endpoint: params.noteableData.create_note_path,
data: postData, data,
}; };
} }
......
...@@ -6,10 +6,13 @@ import mrPageModule from './modules'; ...@@ -6,10 +6,13 @@ import mrPageModule from './modules';
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ export const createStore = () =>
modules: { new Vuex.Store({
page: mrPageModule, modules: {
notes: notesModule(), page: mrPageModule,
diffs: diffsModule(), notes: notesModule(),
}, diffs: diffsModule(),
}); },
});
export default createStore();
...@@ -7,10 +7,14 @@ import editSvg from 'icons/_icon_pencil.svg'; ...@@ -7,10 +7,14 @@ 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 Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'NoteActions', name: 'NoteActions',
components: {
Icon,
},
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -20,7 +24,7 @@ export default { ...@@ -20,7 +24,7 @@ export default {
required: true, required: true,
}, },
noteId: { noteId: {
type: String, type: [String, Number],
required: true, required: true,
}, },
noteUrl: { noteUrl: {
...@@ -35,7 +39,8 @@ export default { ...@@ -35,7 +39,8 @@ export default {
}, },
reportAbusePath: { reportAbusePath: {
type: String, type: String,
required: true, required: false,
default: null,
}, },
canEdit: { canEdit: {
type: Boolean, type: Boolean,
...@@ -84,6 +89,9 @@ export default { ...@@ -84,6 +89,9 @@ export default {
shouldShowActionsDropdown() { shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse); return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
}, },
showDeleteAction() {
return this.canDelete && !this.canReportAsAbuse && !this.noteUrl;
},
isAuthoredByCurrentUser() { isAuthoredByCurrentUser() {
return this.authorId === this.currentUserId; return this.authorId === this.currentUserId;
}, },
...@@ -201,7 +209,26 @@ export default { ...@@ -201,7 +209,26 @@ export default {
</button> </button>
</div> </div>
<div <div
v-if="shouldShowActionsDropdown" v-if="showDeleteAction"
class="note-actions-item"
>
<button
v-tooltip
type="button"
title="Delete comment"
class="note-action-button js-note-delete btn btn-transparent"
data-container="body"
data-placement="bottom"
@click="onDelete"
>
<icon
name="remove"
class="link-highlight"
/>
</button>
</div>
<div
v-else-if="shouldShowActionsDropdown"
class="dropdown more-actions note-actions-item"> class="dropdown more-actions note-actions-item">
<button <button
v-tooltip v-tooltip
......
...@@ -109,7 +109,7 @@ export default { ...@@ -109,7 +109,7 @@ export default {
class="note_edited_ago" class="note_edited_ago"
/> />
<note-awards-list <note-awards-list
v-if="note.award_emoji.length" v-if="note.award_emoji && note.award_emoji.length"
:note-id="note.id" :note-id="note.id"
:note-author-id="note.author.id" :note-author-id="note.author.id"
:awards="note.award_emoji" :awards="note.award_emoji"
......
...@@ -20,7 +20,7 @@ export default { ...@@ -20,7 +20,7 @@ export default {
default: '', default: '',
}, },
noteId: { noteId: {
type: String, type: [String, Number],
required: false, required: false,
default: '', default: '',
}, },
......
...@@ -14,7 +14,8 @@ export default { ...@@ -14,7 +14,8 @@ export default {
}, },
createdAt: { createdAt: {
type: String, type: String,
required: true, required: false,
default: null,
}, },
actionText: { actionText: {
type: String, type: String,
...@@ -22,8 +23,9 @@ export default { ...@@ -22,8 +23,9 @@ export default {
default: '', default: '',
}, },
noteId: { noteId: {
type: String, type: [String, Number],
required: true, required: false,
default: null,
}, },
includeToggle: { includeToggle: {
type: Boolean, type: Boolean,
...@@ -96,18 +98,22 @@ export default { ...@@ -96,18 +98,22 @@ export default {
<span class="system-note-message"> <span class="system-note-message">
<slot></slot> <slot></slot>
</span> </span>
<span class="system-note-separator"> <template
&middot; v-if="createdAt"
</span> >
<a <span class="system-note-separator">
:href="noteTimestampLink" &middot;
class="note-timestamp system-note-separator" </span>
@click="updateTargetNoteHash"> <a
<time-ago-tooltip :href="noteTimestampLink"
:time="createdAt" class="note-timestamp system-note-separator"
tooltip-placement="bottom" @click="updateTargetNoteHash">
/> <time-ago-tooltip
</a> :time="createdAt"
tooltip-placement="bottom"
/>
</a>
</template>
<i <i
class="fa fa-spinner fa-spin editing-spinner" class="fa fa-spinner fa-spin editing-spinner"
aria-label="Comment is being updated" aria-label="Comment is being updated"
......
...@@ -52,7 +52,7 @@ export default { ...@@ -52,7 +52,7 @@ export default {
return this.note.resolvable && !!this.getUserData.id; return this.note.resolvable && !!this.getUserData.id;
}, },
canReportAsAbuse() { canReportAsAbuse() {
return this.note.report_abuse_path && this.author.id !== this.getUserData.id; return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
}, },
noteAnchorId() { noteAnchorId() {
return `note_${this.note.id}`; return `note_${this.note.id}`;
...@@ -81,13 +81,17 @@ export default { ...@@ -81,13 +81,17 @@ export default {
...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']), ...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']),
editHandler() { editHandler() {
this.isEditing = true; this.isEditing = true;
this.$emit('handleEdit');
}, },
deleteHandler() { deleteHandler() {
const typeOfComment = this.note.isDraft ? 'pending comment' : 'comment';
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to delete this comment?')) { if (window.confirm(`Are you sure you want to delete this ${typeOfComment}?`)) {
this.isDeleting = true; this.isDeleting = true;
this.$emit('handleDeleteNote', this.note); this.$emit('handleDeleteNote', this.note);
if (this.note.isDraft) return;
this.deleteNote(this.note) this.deleteNote(this.note)
.then(() => { .then(() => {
this.isDeleting = false; this.isDeleting = false;
...@@ -98,7 +102,20 @@ export default { ...@@ -98,7 +102,20 @@ export default {
}); });
} }
}, },
updateSuccess() {
this.isEditing = false;
this.isRequesting = false;
this.oldContent = null;
$(this.$refs.noteBody.$el).renderGFM();
this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
formUpdateHandler(noteText, parentElement, callback) { formUpdateHandler(noteText, parentElement, callback) {
this.$emit('handleUpdateNote', {
note: this.note,
noteText,
callback: () => this.updateSuccess(),
});
const data = { const data = {
endpoint: this.note.path, endpoint: this.note.path,
note: { note: {
...@@ -113,11 +130,7 @@ export default { ...@@ -113,11 +130,7 @@ export default {
this.updateNote(data) this.updateNote(data)
.then(() => { .then(() => {
this.isEditing = false; this.updateSuccess();
this.isRequesting = false;
this.oldContent = null;
$(this.$refs.noteBody.$el).renderGFM();
this.$refs.noteBody.resetAutoSave();
callback(); callback();
}) })
.catch(() => { .catch(() => {
...@@ -142,6 +155,7 @@ export default { ...@@ -142,6 +155,7 @@ export default {
this.oldContent = null; this.oldContent = null;
} }
this.isEditing = false; this.isEditing = false;
this.$emit('cancelForm');
}, },
recoverNoteContent(noteText) { recoverNoteContent(noteText) {
// we need to do this to prevent noteForm inconsistent content warning // we need to do this to prevent noteForm inconsistent content warning
......
...@@ -150,11 +150,24 @@ export const toggleIssueLocalState = ({ commit }, newState) => { ...@@ -150,11 +150,24 @@ export const toggleIssueLocalState = ({ commit }, newState) => {
export const saveNote = ({ commit, dispatch }, noteData) => { export const saveNote = ({ commit, dispatch }, noteData) => {
// For MR discussuions we need to post as `note[note]` and issue we use `note.note`. // For MR discussuions we need to post as `note[note]` and issue we use `note.note`.
const note = noteData.data['note[note]'] || noteData.data.note.note; // For batch comments, we use draft_note
const note = noteData.data.draft_note || noteData.data['note[note]'] || noteData.data.note.note;
let placeholderText = note; let placeholderText = note;
const hasQuickActions = utils.hasQuickActions(placeholderText); const hasQuickActions = utils.hasQuickActions(placeholderText);
const replyId = noteData.data.in_reply_to_discussion_id; const replyId = noteData.data.in_reply_to_discussion_id;
const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; let methodToDispatch;
const postData = Object.assign({}, noteData);
if (postData.isDraft === true) {
methodToDispatch = replyId
? 'batchComments/addDraftToDiscussion'
: 'batchComments/createNewDraft';
if (!postData.draft_note && noteData.note) {
postData.draft_note = postData.note;
delete postData.note;
}
} else {
methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote';
}
$('.notes-form .flash-container').hide(); // hide previous flash notification $('.notes-form .flash-container').hide(); // hide previous flash notification
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
...@@ -180,7 +193,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -180,7 +193,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
} }
} }
return dispatch(methodToDispatch, noteData).then(res => { return dispatch(methodToDispatch, postData, { root: true }).then(res => {
const { errors } = res; const { errors } = res;
const commandsChanges = res.commands_changes; const commandsChanges = res.commands_changes;
......
...@@ -74,6 +74,9 @@ export const allDiscussions = (state, getters) => { ...@@ -74,6 +74,9 @@ export const allDiscussions = (state, getters) => {
return Object.values(resolved).concat(unresolved); return Object.values(resolved).concat(unresolved);
}; };
export const isDiscussionResolved = (state, getters) => discussionId =>
getters.resolvedDiscussionsById[discussionId] !== undefined;
export const allResolvableDiscussions = (state, getters) => export const allResolvableDiscussions = (state, getters) =>
getters.allDiscussions.filter(d => !d.individual_note && d.resolvable); getters.allDiscussions.filter(d => !d.individual_note && d.resolvable);
......
...@@ -9,8 +9,7 @@ ...@@ -9,8 +9,7 @@
padding-left: $contextual-sidebar-width; padding-left: $contextual-sidebar-width;
} }
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
.issuable-sidebar-header {
padding: 10px 0 15px; padding: 10px 0 15px;
} }
} }
...@@ -75,7 +74,7 @@ ...@@ -75,7 +74,7 @@
.nav-sidebar { .nav-sidebar {
transition: width $sidebar-transition-duration, left $sidebar-transition-duration; transition: width $sidebar-transition-duration, left $sidebar-transition-duration;
position: fixed; position: fixed;
z-index: 400; z-index: 600;
width: $contextual-sidebar-width; width: $contextual-sidebar-width;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
...@@ -86,8 +85,7 @@ ...@@ -86,8 +85,7 @@
&:not(.sidebar-collapsed-desktop) { &:not(.sidebar-collapsed-desktop) {
@media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) {
box-shadow: inset -1px 0 0 $border-color, box-shadow: inset -1px 0 0 $border-color, 2px 1px 3px $dropdown-shadow-color;
2px 1px 3px $dropdown-shadow-color;
} }
} }
......
...@@ -343,6 +343,10 @@ ul.notes { ...@@ -343,6 +343,10 @@ ul.notes {
&.parallel { &.parallel {
border-width: 1px; border-width: 1px;
&.new {
border-right-width: 0;
}
} }
.discussion-notes { .discussion-notes {
...@@ -738,7 +742,7 @@ ul.notes { ...@@ -738,7 +742,7 @@ ul.notes {
padding-top: 0; padding-top: 0;
.discussion-wrapper { .discussion-wrapper {
border-color: transparent; border: 0;
} }
} }
} }
......
...@@ -178,7 +178,7 @@ module NotesHelper ...@@ -178,7 +178,7 @@ module NotesHelper
notesPath: notes_url, notesPath: notes_url,
totalNotes: issuable.discussions.length, totalNotes: issuable.discussions.length,
lastFetchedAt: Time.now.to_i lastFetchedAt: Time.now.to_i
}.to_json }
end end
def discussion_resolved_intro(discussion) def discussion_resolved_intro(discussion)
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%section.js-vue-notes-event %section.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue), #js-vue-notes{ data: { notes_data: notes_data(@issue).to_json,
noteable_data: serialize_issuable(@issue), noteable_data: serialize_issuable(@issue),
noteable_type: 'Issue', noteable_type: 'Issue',
target_type: 'issue', target_type: 'issue',
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
%section.col-md-12 %section.col-md-12
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe %script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event .issuable-discussion.js-vue-notes-event
#js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request), #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json,
noteable_data: serialize_issuable(@merge_request), noteable_data: serialize_issuable(@merge_request),
noteable_type: 'MergeRequest', noteable_type: 'MergeRequest',
target_type: 'merge_request', target_type: 'merge_request',
......
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