Commit a51457a3 authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Natalia Tepluhina

Convert to a Vue instance

Make sidebar Vuex store available

Add mr_notes/stores

Add issuable warning header to mr

Also separate issuable warning header initializer.

Use graphql to update lock status for mr

Separate some changes into a separate MR.

Use graphql for locking/unlocking issue

Set discussion_locked to false if it's null initially.

Update the edit form related specs

Remove mediator and comments

Also remove wait_for_promises helper

Add errors field for consistency
parent afdea89e
...@@ -20,6 +20,11 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -20,6 +20,11 @@ document.addEventListener('DOMContentLoaded', () => {
noteableData.noteableType = notesDataset.noteableType; noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType; noteableData.targetType = notesDataset.targetType;
if (noteableData.discussion_locked === null) {
// discussion_locked has never been set for this issuable.
// set to `false` for safety.
noteableData.discussion_locked = false;
}
if (parsedUserData) { if (parsedUserData) {
currentUserData = { currentUserData = {
......
...@@ -13,7 +13,9 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; ...@@ -13,7 +13,9 @@ import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils'; import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import { mergeUrlParams } from '../../lib/utils/url_utility'; import { mergeUrlParams } from '../../lib/utils/url_utility';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql'; import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import Api from '~/api'; import Api from '~/api';
...@@ -42,6 +44,30 @@ export const updateConfidentialityOnIssue = ({ commit, getters }, { confidential ...@@ -42,6 +44,30 @@ export const updateConfidentialityOnIssue = ({ commit, getters }, { confidential
}); });
}; };
export const updateLockedAttribute = ({ commit, getters }, { locked, fullPath }) => {
const { iid, targetType } = getters.getNoteableData;
return utils.gqClient
.mutate({
mutation: targetType === 'issue' ? updateIssueLockMutation : updateMergeRequestLockMutation,
variables: {
input: {
projectPath: fullPath,
iid: String(iid),
locked,
},
},
})
.then(({ data }) => {
const discussionLocked =
targetType === 'issue'
? data.issueSetLocked.issue.discussionLocked
: data.mergeRequestSetLocked.mergeRequest.discussionLocked;
commit(types.SET_ISSUABLE_LOCK, discussionLocked);
});
};
export const expandDiscussion = ({ commit, dispatch }, data) => { export const expandDiscussion = ({ commit, dispatch }, data) => {
if (data.discussionId) { if (data.discussionId) {
dispatch('diffs/renderFileForDiscussionId', data.discussionId, { root: true }); dispatch('diffs/renderFileForDiscussionId', data.discussionId, { root: true });
......
...@@ -42,6 +42,7 @@ export const REOPEN_ISSUE = 'REOPEN_ISSUE'; ...@@ -42,6 +42,7 @@ export const REOPEN_ISSUE = 'REOPEN_ISSUE';
export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING'; export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING'; export const TOGGLE_BLOCKED_ISSUE_WARNING = 'TOGGLE_BLOCKED_ISSUE_WARNING';
export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL'; export const SET_ISSUE_CONFIDENTIAL = 'SET_ISSUE_CONFIDENTIAL';
export const SET_ISSUABLE_LOCK = 'SET_ISSUABLE_LOCK';
// Description version // Description version
export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION'; export const REQUEST_DESCRIPTION_VERSION = 'REQUEST_DESCRIPTION_VERSION';
......
...@@ -99,6 +99,10 @@ export default { ...@@ -99,6 +99,10 @@ export default {
state.noteableData.confidential = data; state.noteableData.confidential = data;
}, },
[types.SET_ISSUABLE_LOCK](state, locked) {
state.noteableData.discussion_locked = locked;
},
[types.SET_USER_DATA](state, data) { [types.SET_USER_DATA](state, data) {
Object.assign(state, { userData: data }); Object.assign(state, { userData: data });
}, },
......
...@@ -3,5 +3,6 @@ mutation updateIssueConfidential($input: IssueSetConfidentialInput!) { ...@@ -3,5 +3,6 @@ mutation updateIssueConfidential($input: IssueSetConfidentialInput!) {
issue { issue {
confidential confidential
} }
errors
} }
} }
<script> <script>
import editFormButtons from './edit_form_buttons.vue'; import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import { __, sprintf } from '../../../locale'; import { __, sprintf } from '../../../locale';
export default { export default {
components: { components: {
editFormButtons, editFormButtons,
}, },
mixins: [issuableMixin],
props: { props: {
isLocked: { isLocked: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
issuableDisplayName: {
updateLockedAttribute: {
required: true, required: true,
type: Function, type: String,
}, },
}, },
computed: { computed: {
...@@ -42,12 +39,12 @@ export default { ...@@ -42,12 +39,12 @@ export default {
<template> <template>
<div class="dropdown show"> <div class="dropdown show">
<div class="dropdown-menu sidebar-item-warning-message"> <div class="dropdown-menu sidebar-item-warning-message" data-testid="warning-text">
<p v-if="isLocked" class="text" v-html="unlockWarning"></p> <p v-if="isLocked" class="text" v-html="unlockWarning"></p>
<p v-else class="text" v-html="lockWarning"></p> <p v-else class="text" v-html="lockWarning"></p>
<edit-form-buttons :is-locked="isLocked" :update-locked-attribute="updateLockedAttribute" /> <edit-form-buttons :is-locked="isLocked" :issuable-display-name="issuableDisplayName" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { __ } from '~/locale'; import { GlLoadingIcon } from '@gitlab/ui';
import { __, sprintf } from '../../../locale';
import Flash from '~/flash';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import { mapActions } from 'vuex';
export default { export default {
components: {
GlLoadingIcon,
},
inject: ['fullPath'],
props: { props: {
isLocked: { isLocked: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
issuableDisplayName: {
updateLockedAttribute: {
required: true, required: true,
type: Function, type: String,
}, },
}, },
data() {
return {
isLoading: false,
};
},
computed: { computed: {
buttonText() { buttonText() {
return this.isLocked ? __('Unlock') : __('Lock'); if (this.isLoading) {
}, return __('Applying');
}
toggleLock() { return this.isLocked ? __('Unlock') : __('Lock');
return !this.isLocked;
}, },
}, },
methods: { methods: {
...mapActions(['updateLockedAttribute']),
closeForm() { closeForm() {
eventHub.$emit('closeLockForm'); eventHub.$emit('closeLockForm');
$(this.$el).trigger('hidden.gl.dropdown'); $(this.$el).trigger('hidden.gl.dropdown');
}, },
submitForm() { submitForm() {
this.isLoading = true;
this.updateLockedAttribute({
locked: !this.isLocked,
fullPath: this.fullPath,
})
.catch(() => {
const flashMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
Flash(sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }));
})
.finally(() => {
this.closeForm(); this.closeForm();
this.updateLockedAttribute(this.toggleLock); this.isLoading = false;
});
}, },
}, },
}; };
...@@ -45,7 +69,14 @@ export default { ...@@ -45,7 +69,14 @@ export default {
{{ __('Cancel') }} {{ __('Cancel') }}
</button> </button>
<button type="button" class="btn btn-close" @click.prevent="submitForm"> <button
type="button"
data-testid="lock-toggle"
class="btn btn-close"
:disabled="isLoading"
@click.prevent="submitForm"
>
<gl-loading-icon v-if="isLoading" inline />
{{ buttonText }} {{ buttonText }}
</button> </button>
</div> </div>
......
<script> <script>
import { __, sprintf } from '~/locale'; import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub'; import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import { mapGetters } from 'vuex';
export default { export default {
issue: 'issue',
locked: {
icon: 'lock',
class: 'value',
iconClass: 'is-active',
displayText: __('Locked'),
},
unlocked: {
class: ['no-value hide-collapsed'],
icon: 'lock-open',
iconClass: '',
displayText: __('Unlocked'),
},
components: { components: {
editForm, editForm,
Icon, Icon,
...@@ -17,35 +29,28 @@ export default { ...@@ -17,35 +29,28 @@ export default {
tooltip, tooltip,
}, },
mixins: [issuableMixin],
props: { props: {
isLocked: {
required: true,
type: Boolean,
},
isEditable: { isEditable: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
}, },
data() {
return {
isLockDialogOpen: false,
};
}, },
computed: { computed: {
lockIcon() { ...mapGetters(['getNoteableData']),
return this.isLocked ? 'lock' : 'lock-open'; issuableDisplayName() {
const isInIssuePage = this.getNoteableData.targetType === this.$options.issue;
return isInIssuePage ? __('issue') : __('merge request');
}, },
isLocked() {
isLockDialogOpen() { return this.getNoteableData.discussion_locked;
return this.mediator.store.isLockDialogOpen; },
lockStatus() {
return this.isLocked ? this.$options.locked : this.$options.unlocked;
}, },
tooltipLabel() { tooltipLabel() {
...@@ -64,28 +69,9 @@ export default { ...@@ -64,28 +69,9 @@ export default {
methods: { methods: {
toggleForm() { toggleForm() {
if (this.isEditable) { if (this.isEditable) {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; this.isLockDialogOpen = !this.isLockDialogOpen;
} }
}, },
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
discussion_locked: locked,
})
.then(() => window.location.reload())
.catch(() =>
Flash(
sprintf(
__(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
),
{
issuableDisplayName: this.issuableDisplayName,
},
),
),
);
},
}, },
}; };
</script> </script>
...@@ -96,12 +82,13 @@ export default { ...@@ -96,12 +82,13 @@ export default {
v-tooltip v-tooltip
:title="tooltipLabel" :title="tooltipLabel"
class="sidebar-collapsed-icon" class="sidebar-collapsed-icon"
data-testid="sidebar-collapse-icon"
data-container="body" data-container="body"
data-placement="left" data-placement="left"
data-boundary="viewport" data-boundary="viewport"
@click="toggleForm" @click="toggleForm"
> >
<icon :name="lockIcon" class="sidebar-item-icon is-active" /> <icon :name="lockStatus.icon" class="sidebar-item-icon is-active" />
</div> </div>
<div class="title hide-collapsed"> <div class="title hide-collapsed">
...@@ -110,6 +97,7 @@ export default { ...@@ -110,6 +97,7 @@ export default {
v-if="isEditable" v-if="isEditable"
class="float-right lock-edit" class="float-right lock-edit"
href="#" href="#"
data-testid="edit-link"
data-track-event="click_edit_button" data-track-event="click_edit_button"
data-track-label="right_sidebar" data-track-label="right_sidebar"
data-track-property="lock_issue" data-track-property="lock_issue"
...@@ -122,18 +110,19 @@ export default { ...@@ -122,18 +110,19 @@ export default {
<div class="value sidebar-item-value hide-collapsed"> <div class="value sidebar-item-value hide-collapsed">
<edit-form <edit-form
v-if="isLockDialogOpen" v-if="isLockDialogOpen"
data-testid="edit-form"
:is-locked="isLocked" :is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute" :issuable-display-name="issuableDisplayName"
:issuable-type="issuableType"
/> />
<div v-if="isLocked" class="value sidebar-item-value"> <div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class">
<icon :size="16" name="lock" class="sidebar-item-icon inline is-active" /> <icon
{{ __('Locked') }} :size="16"
</div> :name="lockStatus.icon"
class="sidebar-item-icon"
<div v-else class="no-value sidebar-item-value hide-collapsed"> :class="lockStatus.iconClass"
<icon :size="16" name="lock-open" class="sidebar-item-icon inline" /> {{ __('Unlocked') }} />
{{ lockStatus.displayText }}
</div> </div>
</div> </div>
</div> </div>
......
mutation updateIssueLocked($input: IssueSetLockedInput!) {
issueSetLocked(input: $input) {
issue {
discussionLocked
}
errors
}
}
mutation updateMergeRequestLocked($input: MergeRequestSetLockedInput!) {
mergeRequestSetLocked(input: $input) {
mergeRequest {
discussionLocked
}
errors
}
}
...@@ -12,6 +12,7 @@ import Translate from '../vue_shared/translate'; ...@@ -12,6 +12,7 @@ import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { store } from '~/notes/stores'; import { store } from '~/notes/stores';
import { isInIssuePage } from '~/lib/utils/common_utils'; import { isInIssuePage } from '~/lib/utils/common_utils';
import mergeRequestStore from '~/mr_notes/stores';
Vue.use(Translate); Vue.use(Translate);
Vue.use(VueApollo); Vue.use(VueApollo);
...@@ -79,24 +80,28 @@ function mountConfidentialComponent(mediator) { ...@@ -79,24 +80,28 @@ function mountConfidentialComponent(mediator) {
}); });
} }
function mountLockComponent(mediator) { function mountLockComponent() {
const el = document.getElementById('js-lock-entry-point'); const el = document.getElementById('js-lock-entry-point');
const { fullPath } = getSidebarOptions();
if (!el) return;
const dataNode = document.getElementById('js-lock-issue-data'); const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML); const initialData = JSON.parse(dataNode.innerHTML);
const LockComp = Vue.extend(LockIssueSidebar); return el
? new Vue({
new LockComp({ el,
propsData: { store: isInIssuePage() ? store : mergeRequestStore,
isLocked: initialData.is_locked, provide: {
fullPath,
},
render: createElement =>
createElement(LockIssueSidebar, {
props: {
isEditable: initialData.is_editable, isEditable: initialData.is_editable,
mediator,
issuableType: isInIssuePage() ? 'issue' : 'merge_request',
}, },
}).$mount(el); }),
})
: undefined;
} }
function mountParticipantsComponent(mediator) { function mountParticipantsComponent(mediator) {
......
---
title: Allow an issue or MR to be locked and unlocked without page refresh
merge_request: 36773
author:
type: changed
...@@ -19,7 +19,9 @@ import { ...@@ -19,7 +19,9 @@ import {
} from '../mock_data'; } from '../mock_data';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as utils from '~/notes/stores/utils'; import * as utils from '~/notes/stores/utils';
import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql'; import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
const TEST_ERROR_MESSAGE = 'Test error message'; const TEST_ERROR_MESSAGE = 'Test error message';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -1263,4 +1265,61 @@ describe('Actions Notes Store', () => { ...@@ -1263,4 +1265,61 @@ describe('Actions Notes Store', () => {
}); });
}); });
}); });
describe.each`
issuableType
${'issue'} | ${'merge_request'}
`('updateLockedAttribute for issuableType=$issuableType', ({ issuableType }) => {
// Payload for mutation query
state = { noteableData: { discussion_locked: false } };
const targetType = issuableType;
const getters = { getNoteableData: { iid: '1', targetType } };
// Target state after mutation
const locked = true;
const actionArgs = { fullPath: 'full/path', locked };
const input = { iid: '1', projectPath: 'full/path', locked: true };
// Helper functions
const targetMutation = () => {
return targetType === 'issue' ? updateIssueLockMutation : updateMergeRequestLockMutation;
};
const mockResolvedValue = () => {
return targetType === 'issue'
? { data: { issueSetLocked: { issue: { discussionLocked: locked } } } }
: { data: { mergeRequestSetLocked: { mergeRequest: { discussionLocked: locked } } } };
};
beforeEach(() => {
jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(mockResolvedValue());
});
it('calls gqClient mutation one time', () => {
actions.updateLockedAttribute({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
});
it('calls gqClient mutation with the correct values', () => {
actions.updateLockedAttribute({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledWith({
mutation: targetMutation(),
variables: { input },
});
});
describe('on success of mutation', () => {
it('calls commit with the correct values', () => {
const commitSpy = jest.fn();
return actions
.updateLockedAttribute({ commit: commitSpy, state, getters }, actionArgs)
.then(() => {
expect(commitSpy).toHaveBeenCalledWith(mutationTypes.SET_ISSUABLE_LOCK, locked);
});
});
});
});
}); });
...@@ -833,13 +833,27 @@ describe('Notes Store mutations', () => { ...@@ -833,13 +833,27 @@ describe('Notes Store mutations', () => {
state = { noteableData: { confidential: false } }; state = { noteableData: { confidential: false } };
}); });
it('sets sort order', () => { it('should set issuable as confidential', () => {
mutations.SET_ISSUE_CONFIDENTIAL(state, true); mutations.SET_ISSUE_CONFIDENTIAL(state, true);
expect(state.noteableData.confidential).toBe(true); expect(state.noteableData.confidential).toBe(true);
}); });
}); });
describe('SET_ISSUABLE_LOCK', () => {
let state;
beforeEach(() => {
state = { noteableData: { discussion_locked: false } };
});
it('should set issuable as locked', () => {
mutations.SET_ISSUABLE_LOCK(state, true);
expect(state.noteableData.discussion_locked).toBe(true);
});
});
describe('UPDATE_ASSIGNEES', () => { describe('UPDATE_ASSIGNEES', () => {
it('should update assignees', () => { it('should update assignees', () => {
const state = { const state = {
......
export const ISSUABLE_TYPE_ISSUE = 'issue';
export const ISSUABLE_TYPE_MR = 'merge request';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue'; import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
import eventHub from '~/sidebar/event_hub';
import flash from '~/flash';
import createStore from '~/notes/stores';
import { createStore as createMrStore } from '~/mr_notes/stores';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
jest.mock('~/flash');
describe('EditFormButtons', () => { describe('EditFormButtons', () => {
let wrapper; let wrapper;
let store;
let issuableType;
let issuableDisplayName;
const setIssuableType = pageType => {
issuableType = pageType;
issuableDisplayName = issuableType.replace(/_/g, ' ');
};
const findLockToggle = () => wrapper.find('[data-testid="lock-toggle"]');
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const createComponent = ({ props = {}, data = {}, resolved = true }) => {
store = issuableType === ISSUABLE_TYPE_ISSUE ? createStore() : createMrStore();
const mountComponent = propsData => shallowMount(EditFormButtons, { propsData }); if (resolved) {
jest.spyOn(store, 'dispatch').mockResolvedValue();
} else {
jest.spyOn(store, 'dispatch').mockRejectedValue();
}
wrapper = shallowMount(EditFormButtons, {
store,
provide: {
fullPath: '',
},
propsData: {
isLocked: false,
issuableDisplayName,
...props,
},
data() {
return {
isLoading: false,
...data,
};
},
});
};
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
}); });
it('displays "Unlock" when locked', () => { describe.each`
wrapper = mountComponent({ pageType
isLocked: true, ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
updateLockedAttribute: () => {}, `('In $pageType page', ({ pageType }) => {
beforeEach(() => {
setIssuableType(pageType);
}); });
expect(wrapper.text()).toContain('Unlock'); describe('when isLoading', () => {
beforeEach(() => {
createComponent({ data: { isLoading: true } });
}); });
it('displays "Lock" when unlocked', () => { it('renders "Applying" in the toggle button', () => {
wrapper = mountComponent({ expect(findLockToggle().text()).toBe('Applying');
isLocked: false, });
updateLockedAttribute: () => {},
it('disables the toggle button', () => {
expect(findLockToggle().attributes('disabled')).toBe('disabled');
});
it('displays the GlLoadingIcon', () => {
expect(findGlLoadingIcon().exists()).toBe(true);
});
}); });
expect(wrapper.text()).toContain('Lock'); describe.each`
isLocked | toggleText | statusText
${false} | ${'Lock'} | ${'unlocked'}
${true} | ${'Unlock'} | ${'locked'}
`('when $statusText', ({ isLocked, toggleText }) => {
beforeEach(() => {
createComponent({
props: {
isLocked,
},
});
});
it(`toggle button displays "${toggleText}"`, () => {
expect(findLockToggle().text()).toContain(toggleText);
});
describe('when toggled', () => {
describe(`when resolved`, () => {
beforeEach(() => {
createComponent({
props: {
isLocked,
},
resolved: true,
});
findLockToggle().trigger('click');
});
it('dispatches the correct action', () => {
expect(store.dispatch).toHaveBeenCalledWith('updateLockedAttribute', {
locked: !isLocked,
fullPath: '',
});
});
it('resets loading', async () => {
await wrapper.vm.$nextTick().then(() => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
});
it('emits close form', () => {
return wrapper.vm.$nextTick().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
});
it('does not flash an error message', () => {
expect(flash).not.toHaveBeenCalled();
});
});
describe(`when not resolved`, () => {
beforeEach(() => {
createComponent({
props: {
isLocked,
},
resolved: false,
});
findLockToggle().trigger('click');
});
it('dispatches the correct action', () => {
expect(store.dispatch).toHaveBeenCalledWith('updateLockedAttribute', {
locked: !isLocked,
fullPath: '',
});
});
it('resets loading', async () => {
await wrapper.vm.$nextTick().then(() => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
});
it('emits close form', () => {
return wrapper.vm.$nextTick().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
});
});
it('calls flash with the correct message', () => {
expect(flash).toHaveBeenCalledWith(
`Something went wrong trying to change the locked state of this ${issuableDisplayName}`,
);
});
});
});
});
}); });
}); });
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import editForm from '~/sidebar/components/lock/edit_form.vue'; import EditForm from '~/sidebar/components/lock/edit_form.vue';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
describe('EditForm', () => { describe('Edit Form Dropdown', () => {
let vm1; let wrapper;
let vm2; let issuableType; // Either ISSUABLE_TYPE_ISSUE or ISSUABLE_TYPE_MR
let issuableDisplayName;
beforeEach(() => { const setIssuableType = pageType => {
const Component = Vue.extend(editForm); issuableType = pageType;
const toggleForm = () => {}; issuableDisplayName = issuableType.replace(/_/g, ' ');
const updateLockedAttribute = () => {}; };
vm1 = new Component({ const findWarningText = () => wrapper.find('[data-testid="warning-text"]');
propsData: {
isLocked: true,
toggleForm,
updateLockedAttribute,
issuableType: 'issue',
},
}).$mount();
vm2 = new Component({ const createComponent = ({ props }) => {
wrapper = shallowMount(EditForm, {
propsData: { propsData: {
isLocked: false, isLocked: false,
toggleForm, issuableDisplayName,
updateLockedAttribute, ...props,
issuableType: 'merge_request',
}, },
}).$mount(); });
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe.each`
pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
`('In $pageType page', ({ pageType }) => {
beforeEach(() => {
setIssuableType(pageType);
}); });
it('renders on the appropriate warning text', () => { describe.each`
expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true); isLocked | lockStatusText | lockAction | warningText
${false} | ${'unlocked'} | ${'Lock'} | ${'Only project members will be able to comment.'}
${true} | ${'locked'} | ${'Unlock'} | ${'Everyone will be able to comment.'}
`('when $lockStatusText', ({ isLocked, lockAction, warningText }) => {
beforeEach(() => {
createComponent({ props: { isLocked } });
});
expect(vm2.$el.innerHTML.includes('Lock this merge request?')).toBe(true); it(`the appropriate warning text is rendered`, () => {
expect(findWarningText().text()).toContain(
`${lockAction} this ${issuableDisplayName}? ${warningText}`,
);
});
});
}); });
}); });
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper'; import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue'; import LockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
import EditForm from '~/sidebar/components/lock/edit_form.vue';
import createStore from '~/notes/stores';
import { createStore as createMrStore } from '~/mr_notes/stores';
import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
describe('LockIssueSidebar', () => { describe('LockIssueSidebar', () => {
let vm1; let wrapper;
let vm2; let store;
let mediator;
let issuableType; // Either ISSUABLE_TYPE_ISSUE or ISSUABLE_TYPE_MR
beforeEach(() => { const setIssuableType = pageType => {
const Component = Vue.extend(lockIssueSidebar); issuableType = pageType;
};
const findSidebarCollapseIcon = () => wrapper.find('[data-testid="sidebar-collapse-icon"]');
const findLockStatus = () => wrapper.find('[data-testid="lock-status"]');
const findEditLink = () => wrapper.find('[data-testid="edit-link"]');
const findEditForm = () => wrapper.find(EditForm);
const mediator = { const initMediator = () => {
mediator = {
service: { service: {
update: Promise.resolve(true), update: Promise.resolve(true),
}, },
store: {},
};
};
store: { const initStore = isLocked => {
isLockDialogOpen: false, if (issuableType === ISSUABLE_TYPE_ISSUE) {
}, store = createStore();
store.getters.getNoteableData.targetType = 'issue';
} else {
store = createMrStore();
}
store.getters.getNoteableData.discussion_locked = isLocked;
}; };
vm1 = new Component({ const createComponent = ({ props = {} }) => {
wrapper = shallowMount(LockIssueSidebar, {
store,
propsData: { propsData: {
isLocked: true,
isEditable: true, isEditable: true,
mediator, mediator,
issuableType: 'issue', ...props,
}, },
}).$mount(); });
};
vm2 = new Component({ afterEach(() => {
propsData: { wrapper.destroy();
isLocked: false, wrapper = null;
isEditable: false,
mediator,
issuableType: 'merge_request',
},
}).$mount();
}); });
it('shows if locked and/or editable', () => { describe.each`
expect(vm1.$el.innerHTML.includes('Edit')).toBe(true); pageType
${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
`('In $pageType page', ({ pageType }) => {
beforeEach(() => {
setIssuableType(pageType);
initMediator();
});
expect(vm1.$el.innerHTML.includes('Locked')).toBe(true); describe.each`
isLocked
${false} | ${true}
`(`renders for isLocked = $isLocked`, ({ isLocked }) => {
beforeEach(() => {
initStore(isLocked);
createComponent({});
});
it('shows the lock status', () => {
expect(findLockStatus().text()).toBe(isLocked ? 'Locked' : 'Unlocked');
});
expect(vm2.$el.innerHTML.includes('Unlocked')).toBe(true); describe('edit form', () => {
let isEditable;
beforeEach(() => {
isEditable = false;
createComponent({ props: { isEditable } });
}); });
it('displays the edit form when editable', done => { describe('when not editable', () => {
expect(vm1.isLockDialogOpen).toBe(false); it('does not display the edit form when opened if not editable', () => {
expect(findEditForm().exists()).toBe(false);
findSidebarCollapseIcon().trigger('click');
vm1.$el.querySelector('.lock-edit').click(); return wrapper.vm.$nextTick().then(() => {
expect(findEditForm().exists()).toBe(false);
});
});
});
expect(vm1.isLockDialogOpen).toBe(true); describe('when editable', () => {
beforeEach(() => {
isEditable = true;
createComponent({ props: { isEditable } });
});
vm1.$nextTick(() => { it('shows the editable status', () => {
expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true); expect(findEditLink().exists()).toBe(isEditable);
expect(findEditLink().text()).toBe('Edit');
});
describe("when 'Edit' is clicked", () => {
it('displays the edit form when editable', () => {
expect(findEditForm().exists()).toBe(false);
findEditLink().trigger('click');
done(); return wrapper.vm.$nextTick().then(() => {
expect(findEditForm().exists()).toBe(true);
}); });
}); });
it('tracks an event when "Edit" is clicked', () => { it('tracks the event ', () => {
const spy = mockTracking('_category_', vm1.$el, jest.spyOn); const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent('.lock-edit'); triggerEvent(findEditLink().element);
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', { expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
label: 'right_sidebar', label: 'right_sidebar',
property: 'lock_issue', property: 'lock_issue',
}); });
}); });
});
it('displays the edit form when opened from collapsed state', done => { describe('When sidebar is collapsed', () => {
expect(vm1.isLockDialogOpen).toBe(false); it('displays the edit form when opened', () => {
expect(findEditForm().exists()).toBe(false);
vm1.$el.querySelector('.sidebar-collapsed-icon').click(); findSidebarCollapseIcon().trigger('click');
expect(vm1.isLockDialogOpen).toBe(true);
setImmediate(() => {
expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
done(); return wrapper.vm.$nextTick().then(() => {
expect(findEditForm().exists()).toBe(true);
});
});
});
});
}); });
}); });
it('does not display the edit form when opened from collapsed state if not editable', done => {
expect(vm2.isLockDialogOpen).toBe(false);
vm2.$el.querySelector('.sidebar-collapsed-icon').click();
Vue.nextTick()
.then(() => {
expect(vm2.isLockDialogOpen).toBe(false);
})
.then(done)
.catch(done.fail);
}); });
}); });
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