Commit 029b9639 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'batch-comments-review-preview' into 'master'

Added previewing of batch comments

Closes #4327

See merge request gitlab-org/gitlab-ee!7936
parents bd83743a 76bca24e
......@@ -192,5 +192,8 @@ export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => {
return getters.unresolvedDiscussionsIdsByDate[0];
};
export const getDiscussion = state => discussionId =>
state.discussions.find(discussion => discussion.id === discussionId);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import NoteableNote from '~/notes/components/noteable_note.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import resolvedStatusMixin from '../mixins/resolved_status';
import PublishButton from './publish_button.vue';
export default {
......@@ -13,6 +13,7 @@ export default {
Icon,
LoadingButton,
},
mixins: [resolvedStatusMixin],
props: {
draft: {
type: Object,
......@@ -26,38 +27,23 @@ export default {
},
computed: {
...mapState('batchComments', ['isPublishing']),
...mapGetters(['isDiscussionResolved']),
...mapGetters('batchComments', ['isPublishingDraft']),
resolvedStatusMessage() {
let message;
const discussionResolved = this.isDiscussionResolved(this.draft.discussion_id);
const discussionToBeResolved = this.draft.resolve_discussion;
if (discussionToBeResolved) {
if (discussionResolved) {
message = s__('MergeRequests|Discussion stays resolved.');
} else {
message = s__('MergeRequests|Discussion will be resolved.');
}
} else if (discussionResolved) {
message = s__('MergeRequests|Discussion will be unresolved.');
} else {
message = s__('MergeRequests|Discussion stays unresolved.');
}
return message;
},
componentClasses() {
return this.draft.resolve_discussion
? 'is-resolving-discussion'
: 'is-unresolving-discussion';
},
draftCommands() {
return this.draft.references.commands;
},
},
mounted() {
if (window.location.hash && window.location.hash === `#note_${this.draft.id}`) {
this.scrollToDraft(this.draft);
}
},
methods: {
...mapActions('batchComments', ['deleteDraft', 'updateDraft', 'publishSingleDraft']),
...mapActions('batchComments', [
'deleteDraft',
'updateDraft',
'publishSingleDraft',
'scrollToDraft',
]),
update(data) {
this.updateDraft(data);
},
......@@ -71,6 +57,7 @@ export default {
this.isEditingDraft = false;
},
},
showStaysResolved: true,
};
</script>
<template>
......@@ -117,6 +104,8 @@ export default {
<p class="draft-note-actions">
<publish-button
:show-count="true"
:should-publish="false"
class="btn btn-success btn-inverted"
/>
<loading-button
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
import { sprintf, n__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import DraftsCount from './drafts_count.vue';
import PublishButton from './publish_button.vue';
import PreviewItem from './preview_item.vue';
export default {
components: {
GlLoadingIcon,
Icon,
DraftsCount,
PublishButton,
PreviewItem,
},
computed: {
...mapGetters(['isNotesFetched']),
...mapGetters('batchComments', ['draftsCount', 'sortedDrafts']),
...mapState('batchComments', ['showPreviewDropdown']),
dropdownTitle() {
return sprintf(
n__('%{count} pending comment', '%{count} pending comments', this.draftsCount),
{ count: this.draftsCount },
);
},
},
watch: {
showPreviewDropdown() {
if (this.showPreviewDropdown && this.$refs.dropdown) {
this.$nextTick(() => this.$refs.dropdown.focus());
}
},
},
mounted() {
document.addEventListener('click', this.onClickDocument);
},
beforeDestroy() {
document.removeEventListener('click', this.onClickDocument);
},
methods: {
...mapActions('batchComments', ['toggleReviewDropdown']),
isLast(index) {
return index === this.sortedDrafts.length - 1;
},
onClickDocument({ target }) {
if (
this.showPreviewDropdown &&
!target.closest('.review-preview-dropdown, .js-publish-draft-button')
) {
this.toggleReviewDropdown();
}
},
},
};
</script>
<template>
<div
class="dropdown float-right review-preview-dropdown"
:class="{
show: showPreviewDropdown
}"
>
<button
ref="dropdown"
type="button"
class="btn btn-success review-preview-dropdown-toggle"
@click="toggleReviewDropdown"
>
{{ __('Finish review') }}
<drafts-count />
<icon
name="angle-up"
/>
</button>
<div
class="dropdown-menu dropdown-menu-large dropdown-menu-right dropdown-open-top"
:class="{
show: showPreviewDropdown
}"
>
<div class="dropdown-title">
{{ dropdownTitle }}
<button
:aria-label="__('Close')"
type="button"
class="dropdown-title-button dropdown-menu-close"
@click="toggleReviewDropdown"
>
<icon
name="close"
/>
</button>
</div>
<div class="dropdown-content">
<ul v-if="isNotesFetched">
<li
v-for="(draft, index) in sortedDrafts"
:key="draft.id"
>
<preview-item
:draft="draft"
:is-last="isLast(index)"
/>
</li>
</ul>
<gl-loading-icon
v-else
:size="2"
class="prepend-top-default append-bottom-default"
/>
</div>
<div class="dropdown-footer">
<publish-button
:show-count="false"
:should-publish="true"
:label="__('Submit review')"
class="float-right append-right-8"
/>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import resolvedStatusMixin from '../mixins/resolved_status';
export default {
components: {
Icon,
},
mixins: [resolvedStatusMixin],
props: {
draft: {
type: Object,
required: true,
},
isLast: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters('diffs', ['getDiffFileByHash']),
...mapGetters(['getDiscussion']),
iconName() {
return this.isDiffDiscussion || this.draft.line_code ? 'doc-text' : 'comment';
},
discussion() {
return this.getDiscussion(this.draft.discussion_id);
},
isDiffDiscussion() {
return this.discussion && this.discussion.diff_discussion;
},
titleText() {
const file = this.discussion ? this.discussion.diff_file : this.draft;
if (file) {
return file.file_path;
}
return sprintf(__("%{authorsName}'s discussion"), {
authorsName: this.discussion.notes.find(note => !note.system).author.name,
});
},
linePosition() {
const position = this.discussion ? this.discussion.position : this.draft.position;
return position.new_line || position.old_line;
},
content() {
const el = document.createElement('div');
el.innerHTML = this.draft.note_html;
return el.textContent;
},
showLinePosition() {
return this.draft.file_hash || this.isDiffDiscussion;
},
},
methods: {
...mapActions('batchComments', ['scrollToDraft']),
},
showStaysResolved: false,
};
</script>
<template>
<button
type="button"
class="review-preview-item menu-item"
:class="[componentClasses, {
'is-last': isLast
}]"
@click="scrollToDraft(draft)"
>
<span class="review-preview-item-header">
<icon
class="append-right-8 flex-shrink-0"
:name="iconName"
/>
<span class="bold">
<span class="review-preview-item-header-text block-truncated">
{{ titleText }}
</span>
<template v-if="showLinePosition">
:{{ linePosition }}
</template>
</span>
</span>
<span class="review-preview-item-content">
<p>{{ content }}</p>
</span>
<span
v-if="draft.discussion_id && resolvedStatusMessage"
class="review-preview-item-footer draft-note-resolution p-0"
>
<icon
class="append-right-8"
name="status_success"
/>
{{ resolvedStatusMessage }}
</span>
</button>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import DraftsCount from './drafts_count.vue';
......@@ -8,11 +9,34 @@ export default {
LoadingButton,
DraftsCount,
},
props: {
showCount: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: __('Finish review'),
},
shouldPublish: {
type: Boolean,
required: true,
},
},
computed: {
...mapState('batchComments', ['isPublishing']),
},
methods: {
...mapActions('batchComments', ['publishReview']),
...mapActions('batchComments', ['publishReview', 'toggleReviewDropdown']),
onClick() {
if (this.shouldPublish) {
this.publishReview();
} else {
this.toggleReviewDropdown();
}
},
},
};
</script>
......@@ -20,12 +44,14 @@ export default {
<template>
<loading-button
:loading="isPublishing"
container-class="btn btn-success"
@click="publishReview"
container-class="btn btn-success js-publish-draft-button"
@click="onClick"
>
<span>
{{ __('Submit review') }}
<drafts-count />
{{ label }}
<drafts-count
v-if="showCount"
/>
</span>
</loading-button>
</template>
......@@ -3,23 +3,31 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import { sprintf, s__ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { GlModal, GlModalDirective } from '@gitlab-org/gitlab-ui';
import PublishButton from './publish_button.vue';
import PreviewDropdown from './preview_dropdown.vue';
export default {
components: {
PublishButton,
LoadingButton,
GlModal,
PreviewDropdown,
},
directives: {
'gl-modal': GlModalDirective,
},
computed: {
...mapGetters(['isNotesFetched']),
...mapState('batchComments', ['isDiscarding']),
...mapGetters('batchComments', ['draftsCount']),
},
watch: {
isNotesFetched() {
if (this.isNotesFetched) {
this.expandAllDiscussions();
}
},
},
methods: {
...mapActions('batchComments', ['discardReview']),
...mapActions('batchComments', ['discardReview', 'expandAllDiscussions']),
},
modalId: 'discard-draft-review',
text: sprintf(
......@@ -38,14 +46,15 @@ export default {
<template>
<div v-show="draftsCount > 0">
<nav class="review-bar-component">
<p class="review-bar-content">
<publish-button />
<div class="review-bar-content">
<preview-dropdown />
<loading-button
v-gl-modal="$options.modalId"
:loading="isDiscarding"
:label="__('Discard review')"
class="float-right"
/>
</p>
</div>
</nav>
<gl-modal
:title="s__('BatchComments|Discard review?')"
......
export const CHANGES_TAB = 'diffs';
export const DISCUSSION_TAB = 'notes';
export const SHOW_TAB = 'show';
import { mapGetters } from 'vuex';
import { s__ } from '~/locale';
export default {
computed: {
...mapGetters(['isDiscussionResolved']),
resolvedStatusMessage() {
let message;
const discussionResolved = this.isDiscussionResolved(this.draft.discussion_id);
const discussionToBeResolved = this.draft.resolve_discussion;
if (discussionToBeResolved && discussionResolved && !this.$options.showStaysResolved) {
return undefined;
}
if (discussionToBeResolved) {
if (discussionResolved) {
message = s__('MergeRequests|Discussion stays resolved.');
} else {
message = s__('MergeRequests|Discussion will be resolved.');
}
} else if (discussionResolved) {
message = s__('MergeRequests|Discussion will be unresolved.');
} else if (this.$options.showStaysResolved) {
message = s__('MergeRequests|Discussion stays unresolved.');
}
return message;
},
componentClasses() {
return this.draft.resolve_discussion
? 'is-resolving-discussion'
: 'is-unresolving-discussion';
},
},
};
import flash from '~/flash';
import { __ } from '~/locale';
import { scrollToElement } from '~/lib/utils/common_utils';
import service from '../../../services/drafts_service';
import * as types from './mutation_types';
import { CHANGES_TAB, DISCUSSION_TAB, SHOW_TAB } from '../../../constants';
export const enableBatchComments = ({ commit }) => {
commit(types.ENABLE_BATCH_COMMENTS);
......@@ -94,5 +96,46 @@ export const updateDraft = ({ commit, getters }, { note, noteText, callback }) =
.then(callback)
.catch(() => flash(__('An error occurred while updating the comment')));
export const scrollToDraft = ({ dispatch, rootGetters }, draft) => {
const discussion = draft.discussion_id && rootGetters.getDiscussion(draft.discussion_id);
const tab =
draft.file_hash || (discussion && discussion.diff_discussion) ? CHANGES_TAB : SHOW_TAB;
const tabEl = tab === CHANGES_TAB ? CHANGES_TAB : DISCUSSION_TAB;
const draftID = `note_${draft.id}`;
const el = document.querySelector(`#${tabEl} #${draftID}`);
dispatch('closeReviewDropdown');
window.location.hash = draftID;
if (window.mrTabs.currentAction !== tab) {
window.mrTabs.tabShown(tab);
}
if (discussion) {
dispatch('expandDiscussion', { discussionId: discussion.id }, { root: true });
}
if (el) {
setTimeout(() => scrollToElement(el.closest('.draft-note-component')));
}
};
export const toggleReviewDropdown = ({ dispatch, state }) => {
if (state.showPreviewDropdown) {
dispatch('closeReviewDropdown');
} else {
dispatch('openReviewDropdown');
}
};
export const openReviewDropdown = ({ commit }) => commit(types.OPEN_REVIEW_DROPDOWN);
export const closeReviewDropdown = ({ commit }) => commit(types.CLOSE_REVIEW_DROPDOWN);
export const expandAllDiscussions = ({ dispatch, state }) =>
state.drafts.filter(draft => draft.discussion_id).forEach(draft => {
dispatch('expandDiscussion', { discussionId: draft.discussion_id }, { root: true });
});
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -64,5 +64,7 @@ export const draftForLine = (state, getters) => (diffFileSha, line, side = null)
export const isPublishingDraft = state => draftId =>
state.currentlyPublishingDrafts.indexOf(draftId) !== -1;
export const sortedDrafts = state => [...state.drafts].sort((a, b) => a.id > b.id);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
......@@ -16,3 +16,6 @@ export const RECEIVE_DISCARD_REVIEW_SUCCESS = 'RECEIVE_DISCARD_REVIEW_SUCCESS';
export const RECEIVE_DISCARD_REVIEW_ERROR = 'RECEIVE_DISCARD_REVIEW_ERROR';
export const RECEIVE_DRAFT_UPDATE_SUCCESS = 'RECEIVE_DRAFT_UPDATE_SUCCESS';
export const OPEN_REVIEW_DROPDOWN = 'OPEN_REVIEW_DROPDOWN';
export const CLOSE_REVIEW_DROPDOWN = 'CLOSE_REVIEW_DROPDOWN';
......@@ -64,4 +64,10 @@ export default {
state.drafts.splice(index, 1, processDraft(data));
}
},
[types.OPEN_REVIEW_DROPDOWN](state) {
state.showPreviewDropdown = true;
},
[types.CLOSE_REVIEW_DROPDOWN](state) {
state.showPreviewDropdown = false;
},
};
......@@ -5,4 +5,5 @@ export default () => ({
isPublishing: false,
currentlyPublishingDrafts: [],
isDiscarding: false,
showPreviewDropdown: false,
});
......@@ -23,18 +23,6 @@
}
}
&.is-resolving-discussion {
.draft-note-resolution svg {
color: $green-600;
}
}
&.is-unresolving-discussion {
.draft-note-resolution svg {
color: $gray-darkest;
}
}
.referenced-commands.draft-note-commands {
background: $orange-100;
font-size: $label-font-size;
......@@ -63,8 +51,9 @@ button[disabled] {
display: flex;
justify-content: space-between;
align-items: center;
}
.draft-note-resolution {
.draft-note-resolution {
padding: $gl-padding-4 $gl-padding;
line-height: 1;
font-size: $label-font-size;
......@@ -75,6 +64,17 @@ button[disabled] {
vertical-align: text-bottom;
display: inline-block;
}
.is-resolving-discussion & {
svg {
color: $green-600;
}
}
.is-unresolving-discussion & {
svg {
color: $gray-darkest;
}
}
}
......
......@@ -8,6 +8,6 @@ $drafts-count-size: 1.6;
line-height: $drafts-count-size;
background: $black-transparent;
border-radius: 50%;
margin-left: 0.5em;
margin-left: $gl-padding-4;
padding: 0 $gl-padding-4;
}
......@@ -4,7 +4,7 @@
left: 0;
width: 100%;
background: $white-light;
z-index: 100;
z-index: 300;
padding: 7px 0 6px; // to keep aligned with "collapse sidebar" button on the left sidebar
border-top: 1px solid $border-color;
padding-left: $contextual-sidebar-width;
......@@ -24,24 +24,99 @@
padding-right: 0;
}
p {
@include clearfix;
.dropdown {
margin-left: $grid-size;
}
}
.review-bar-content {
max-width: $limited-layout-width;
padding: 0 $gl-padding;
width: 100%;
margin: 0 auto;
text-align: right;
}
.review-preview-dropdown {
.review-preview-item.menu-item {
display: flex;
flex-wrap: wrap;
padding: 8px 16px;
cursor: pointer;
&:not(.is-last) {
border-bottom: 1px solid $list-border;
}
}
.dropdown-menu {
top: auto;
bottom: 36px;
&.show {
max-height: 400px;
@include media-breakpoint-down(xs) {
width: calc(100vw - 32px);
}
}
}
.dropdown-content {
max-height: 300px;
}
.btn {
float: right;
.dropdown-title {
padding: $grid-size 25px $gl-padding;
margin-bottom: 0;
}
+ .btn {
margin-right: $grid-size;
.dropdown-footer {
margin-top: 0;
}
.dropdown-menu-close {
top: 6px;
}
}
.review-bar-content {
max-width: $limited-layout-width;
padding: 0 $gl-padding;
.review-preview-dropdown-toggle {
svg.s16 {
width: 15px;
height: 15px;
margin-top: -1px;
top: 3px;
margin-left: 4px;
}
}
.review-preview-item-header {
display: flex;
align-items: center;
width: 100%;
margin: 0 auto;
margin-bottom: 4px;
> .bold {
display: flex;
min-width: 0;
line-height: 16px;
}
}
.review-preview-item-footer {
display: flex;
align-items: center;
margin-top: 4px;
}
.review-preview-item-content {
width: 100%;
p {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0;
}
}
......@@ -80,6 +80,12 @@ class DraftNote < ActiveRecord::Base
Digest::SHA1.hexdigest(diff_file.file_path)
end
def file_path
return unless diff_file
diff_file.file_path
end
def publish_params
attrs = PUBLISH_ATTRS.dup
attrs.concat(DIFF_ATTRS) if on_diff?
......
......@@ -8,6 +8,7 @@ class DraftNoteEntity < Grape::Entity
expose :position, if: -> (note, _) { note.on_diff? }
expose :line_code
expose :file_hash
expose :file_path
expose :note
expose :rendered_note, as: :note_html
expose :references
......
---
title: Enable previewing of draft review comments
merge_request: 7936
author:
type: added
......@@ -53,6 +53,7 @@ describe 'Merge request > Batch comments', :js do
write_comment
page.within('.review-bar-content') do
click_button 'Finish review'
click_button 'Submit review'
end
......@@ -155,6 +156,7 @@ describe 'Merge request > Batch comments', :js do
write_reply_to_discussion(resolve: true)
page.within('.review-bar-content') do
click_button 'Finish review'
click_button 'Submit review'
end
......@@ -197,6 +199,7 @@ describe 'Merge request > Batch comments', :js do
write_reply_to_discussion(button_text: 'Start a review', unresolve: true)
page.within('.review-bar-content') do
click_button 'Finish review'
click_button 'Submit review'
end
......
import Vue from 'vue';
import PreviewItem from 'ee/batch_comments/components/preview_item.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createStore } from '~/mr_notes/stores';
import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
describe('Batch comments draft preview item component', () => {
let vm;
let Component;
let draft;
function createComponent(isLast = false, extra = {}, extendStore = () => {}) {
const store = createStore();
extendStore(store);
draft = {
...createDraft(),
...extra,
};
vm = mountComponentWithStore(Component, { store, props: { draft, isLast } });
}
beforeAll(() => {
Component = Vue.extend(PreviewItem);
});
afterEach(() => {
vm.$destroy();
});
it('renders text content', () => {
createComponent(false, { note_html: '<img src="" /><p>Hello world</p>' });
expect(vm.$el.querySelector('.review-preview-item-content').innerHTML).toEqual(
'<p>Hello world</p>',
);
});
it('adds is last class', () => {
createComponent(true);
expect(vm.$el.classList).toContain('is-last');
});
it('scrolls to draft on click', () => {
createComponent();
spyOn(vm.$store, 'dispatch').and.stub();
vm.$el.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('batchComments/scrollToDraft', vm.draft);
});
describe('for file', () => {
it('renders file path', () => {
createComponent(false, { file_path: 'index.js', file_hash: 'abc', position: {} });
expect(vm.$el.querySelector('.review-preview-item-header-text').textContent).toContain(
'index.js',
);
});
it('renders new line position', () => {
createComponent(false, {
file_path: 'index.js',
file_hash: 'abc',
position: { new_line: 1 },
});
expect(vm.$el.querySelector('.bold').textContent).toContain(':1');
});
it('renders old line position', () => {
createComponent(false, {
file_path: 'index.js',
file_hash: 'abc',
position: { old_line: 2 },
});
expect(vm.$el.querySelector('.bold').textContent).toContain(':2');
});
});
describe('for discussion', () => {
beforeEach(() => {
createComponent(false, { discussion_id: '1', resolve_discussion: true }, store => {
store.state.notes.discussions.push({
id: '1',
notes: [
{
author: {
name: 'Author Name',
},
},
],
});
});
});
it('renders title', () => {
expect(vm.$el.querySelector('.review-preview-item-header-text').textContent).toContain(
"Author Name's discussion",
);
});
it('it renders discussion resolved text', () => {
expect(vm.$el.querySelector('.draft-note-resolution').textContent).toContain(
'Discussion will be resolved',
);
});
});
});
......@@ -3,7 +3,7 @@ import PublishButton from 'ee/batch_comments/components/publish_button.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createStore } from 'ee/batch_comments/stores';
describe('Batch comments drafts count component', () => {
describe('Batch comments publish button component', () => {
let vm;
let Component;
......@@ -14,7 +14,7 @@ describe('Batch comments drafts count component', () => {
beforeEach(() => {
const store = createStore();
vm = mountComponentWithStore(Component, { store });
vm = mountComponentWithStore(Component, { store, props: { shouldPublish: true } });
spyOn(vm.$store, 'dispatch').and.stub();
});
......@@ -26,9 +26,17 @@ describe('Batch comments drafts count component', () => {
it('dispatches publishReview on click', () => {
vm.$el.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith('batchComments/publishReview', undefined);
});
it('dispatches toggleReviewDropdown when shouldPublish is false on click', () => {
vm.shouldPublish = false;
vm.$el.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith(
'batchComments/publishReview',
jasmine.anything(),
'batchComments/toggleReviewDropdown',
undefined,
);
});
......
import Vue from 'vue';
import PreviewDropdown from 'ee/batch_comments/components/preview_dropdown.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { createStore } from '~/mr_notes/stores';
import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
describe('Batch comments draft preview item component', () => {
let vm;
let Component;
function createComponent(extendStore = () => {}) {
const store = createStore();
store.state.batchComments.drafts.push(createDraft(), { ...createDraft(), id: 2 });
extendStore(store);
vm = mountComponentWithStore(Component, { store });
}
beforeAll(() => {
Component = Vue.extend(PreviewDropdown);
});
afterEach(() => {
vm.$destroy();
});
it('toggles dropdown when clicking button', done => {
createComponent();
spyOn(vm.$store, 'dispatch').and.callThrough();
vm.$el.querySelector('.review-preview-dropdown-toggle').click();
expect(vm.$store.dispatch).toHaveBeenCalledWith(
'batchComments/toggleReviewDropdown',
jasmine.anything(),
);
setTimeout(() => {
expect(vm.$el.classList).toContain('show');
done();
});
});
it('toggles dropdown when clicking body', () => {
createComponent();
vm.$store.state.batchComments.showPreviewDropdown = true;
spyOn(vm.$store, 'dispatch').and.stub();
document.body.click();
expect(vm.$store.dispatch).toHaveBeenCalledWith(
'batchComments/toggleReviewDropdown',
undefined,
);
});
it('renders list of drafts', () => {
createComponent(store => {
Object.assign(store.state.notes, {
isNotesFetched: true,
});
});
expect(vm.$el.querySelectorAll('.dropdown-content li').length).toBe(2);
});
it('adds is-last class to last item', () => {
createComponent(store => {
Object.assign(store.state.notes, {
isNotesFetched: true,
});
});
expect(vm.$el.querySelectorAll('.dropdown-content li')[1].querySelector('.is-last')).not.toBe(
null,
);
});
it('renders draft count in dropdown title', () => {
createComponent();
expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('2 pending comments');
});
it('renders publish button in footer', () => {
createComponent();
expect(vm.$el.querySelector('.dropdown-footer .js-publish-draft-button')).not.toBe(null);
});
});
......@@ -10,6 +10,7 @@ export const createDraft = () => ({
current_user: { can_edit: true, can_award_emoji: false, can_resolve: false },
discussion_id: null,
file_hash: null,
file_path: null,
id: 1,
line_code: null,
merge_request_id: 1,
......@@ -19,4 +20,5 @@ export const createDraft = () => ({
references: { users: [], commands: '' },
resolve_discussion: false,
isDraft: true,
position: null,
});
......@@ -309,4 +309,115 @@ describe('Batch comments store actions', () => {
.catch(done.fail);
});
});
describe('toggleReviewDropdown', () => {
it('dispatches openReviewDropdown', done => {
testAction(
actions.toggleReviewDropdown,
null,
{ showPreviewDropdown: false },
[],
[{ type: 'openReviewDropdown' }],
done,
);
});
it('dispatches closeReviewDropdown when showPreviewDropdown is true', done => {
testAction(
actions.toggleReviewDropdown,
null,
{ showPreviewDropdown: true },
[],
[{ type: 'closeReviewDropdown' }],
done,
);
});
});
describe('openReviewDropdown', () => {
it('commits OPEN_REVIEW_DROPDOWN', done => {
testAction(
actions.openReviewDropdown,
null,
null,
[{ type: 'OPEN_REVIEW_DROPDOWN' }],
[],
done,
);
});
});
describe('closeReviewDropdown', () => {
it('commits CLOSE_REVIEW_DROPDOWN', done => {
testAction(
actions.closeReviewDropdown,
null,
null,
[{ type: 'CLOSE_REVIEW_DROPDOWN' }],
[],
done,
);
});
});
describe('expandAllDiscussions', () => {
it('dispatches expandDiscussion for all drafts', done => {
const state = {
drafts: [
{
discussion_id: '1',
},
],
};
testAction(
actions.expandAllDiscussions,
null,
state,
[],
[
{
type: 'expandDiscussion',
payload: { discussionId: '1' },
},
],
done,
);
});
});
describe('scrollToDraft', () => {
beforeEach(() => {
window.mrTabs = {
currentAction: 'notes',
tabShown: jasmine.createSpy('tabShown'),
};
});
it('scrolls to draft item', () => {
const dispatch = jasmine.createSpy('dispatch');
const rootGetters = {
getDiscussion: () => ({
id: '1',
diff_discussion: true,
}),
};
const draft = {
discussion_id: '1',
id: '2',
};
actions.scrollToDraft({ dispatch, rootGetters }, draft);
expect(dispatch.calls.argsFor(0)).toEqual(['closeReviewDropdown']);
expect(dispatch.calls.argsFor(1)).toEqual([
'expandDiscussion',
{ discussionId: '1' },
{ root: true },
]);
expect(window.mrTabs.tabShown).toHaveBeenCalledWith('diffs');
});
});
});
......@@ -148,4 +148,20 @@ describe('Batch comments mutations', () => {
]);
});
});
describe(types.OPEN_REVIEW_DROPDOWN, () => {
it('sets showPreviewDropdown to true', () => {
mutations[types.OPEN_REVIEW_DROPDOWN](state);
expect(state.showPreviewDropdown).toBe(true);
});
});
describe(types.CLOSE_REVIEW_DROPDOWN, () => {
it('sets showPreviewDropdown to false', () => {
mutations[types.CLOSE_REVIEW_DROPDOWN](state);
expect(state.showPreviewDropdown).toBe(false);
});
});
});
......@@ -113,6 +113,9 @@ msgstr[1] ""
msgid "%{actionText} & %{openOrClose} %{noteable}"
msgstr ""
msgid "%{authorsName}'s discussion"
msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
......@@ -127,6 +130,11 @@ msgid_plural "%{count} participants"
msgstr[0] ""
msgstr[1] ""
msgid "%{count} pending comment"
msgid_plural "%{count} pending comments"
msgstr[0] ""
msgstr[1] ""
msgid "%{filePath} deleted"
msgstr ""
......@@ -3410,6 +3418,9 @@ msgstr ""
msgid "Fingerprints"
msgstr ""
msgid "Finish review"
msgstr ""
msgid "Finished"
msgstr ""
......
......@@ -265,4 +265,12 @@ describe('Getters Notes Store', () => {
expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeFalsy();
});
});
describe('getDiscussion', () => {
it('returns discussion by ID', () => {
state.discussions.push({ id: '1' });
expect(getters.getDiscussion(state)('1')).toEqual({ id: '1' });
});
});
});
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