Commit db04e3ee authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-02-13

parents 7afd29db 41285af4
......@@ -180,6 +180,7 @@ export default class CreateMergeRequestDropdown {
valueAttribute: 'data-text',
},
],
hideOnClick: false,
};
}
......
......@@ -3,7 +3,6 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore';
const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
......@@ -14,5 +13,4 @@ export {
ACTIVE_CLASS,
TEMPLATE_REGEX,
IGNORE_CLASS,
IGNORE_HIDING_CLASS,
};
import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
class DropDown {
constructor(list, config = {}) {
constructor(list, config = { }) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
this.eventWrapper = {};
this.hideOnClick = config.hideOnClick !== false;
if (config.addActiveClassToDropdownButton) {
this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
......@@ -37,15 +38,17 @@ class DropDown {
clickEvent(e) {
if (e.target.tagName === 'UL') return;
if (e.target.classList.contains(IGNORE_CLASS)) return;
if (e.target.closest(`.${IGNORE_CLASS}`)) return;
const selected = utils.closest(e.target, 'LI');
const selected = e.target.closest('li');
if (!selected) return;
this.addSelectedClass(selected);
e.preventDefault();
if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
if (this.hideOnClick) {
this.hide();
}
const listEvent = new CustomEvent('click.dl', {
detail: {
......
......@@ -24,6 +24,51 @@ export default class Issue {
if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
}
// Listen to state changes in the Vue app
document.addEventListener('issuable_vue_app:change', (event) => {
this.updateTopState(event.detail.isClosed, event.detail.data);
});
}
/**
* This method updates the top area of the issue.
*
* Once the issue state changes, either through a click on the top area (jquery)
* or a click on the bottom area (Vue) we need to update the top area.
*
* @param {Boolean} isClosed
* @param {Array} data
* @param {String} issueFailMessage
*/
updateTopState(isClosed, data, issueFailMessage = 'Unable to update this issue at this time.') {
if ('id' in data) {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed);
$(document).trigger('issuable:change', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
this.createMergeRequestDropdown.unavailable();
this.createMergeRequestDropdown.disable();
} else {
// We should check in case a branch was created in another tab
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
}
} else {
flash(issueFailMessage);
}
}
initIssueBtnEventListeners() {
......@@ -44,34 +89,8 @@ export default class Issue {
url = $button.attr('href');
return axios.put(url)
.then(({ data }) => {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
if ('id' in data) {
const isClosed = $button.hasClass('btn-close');
isClosedBadge.toggleClass('hidden', !isClosed);
isOpenBadge.toggleClass('hidden', isClosed);
$(document).trigger('issuable:change', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''));
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
projectIssuesCounter.text(addDelimiter(numProjectIssues));
if (this.createMergeRequestDropdown) {
if (isClosed) {
this.createMergeRequestDropdown.unavailable();
this.createMergeRequestDropdown.disable();
} else {
// We should check in case a branch was created in another tab
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
}
} else {
flash(issueFailMessage);
}
const isClosed = $button.hasClass('btn-close');
this.updateTopState(isClosed, data);
})
.catch(() => flash(issueFailMessage))
.then(() => {
......
......@@ -2,16 +2,18 @@
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __ } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default {
......@@ -22,6 +24,7 @@
discussionLockedWidget,
markdownField,
userAvatarLink,
loadingButton,
},
mixins: [
issuableStateMixin,
......@@ -30,9 +33,6 @@
return {
note: '',
noteType: constants.COMMENT,
// Can't use mapGetters,
// this needs to be in the data object because it belongs to the state
issueState: this.$store.getters.getNoteableData.state,
isSubmitting: false,
isSubmitButtonDisabled: true,
};
......@@ -43,6 +43,7 @@
'getUserData',
'getNoteableData',
'getNotesData',
'issueState',
]),
isLoggedIn() {
return this.getUserData.id;
......@@ -105,7 +106,7 @@
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
this.issueState = isClosed ? constants.CLOSED : constants.REOPENED;
this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
});
this.initAutoSave();
......@@ -117,6 +118,9 @@
'stopPolling',
'restartPolling',
'removePlaceholderNotes',
'closeIssue',
'reopenIssue',
'toggleIssueLocalState',
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
if (!_.isEmpty(note) && !isSubmitting) {
......@@ -126,6 +130,8 @@
}
},
handleSave(withIssueAction) {
this.isSubmitting = true;
if (this.note.length) {
const noteData = {
endpoint: this.endpoint,
......@@ -142,7 +148,6 @@
if (this.noteType === constants.DISCUSSION) {
noteData.data.note.type = constants.DISCUSSION_NOTE;
}
this.isSubmitting = true;
this.note = ''; // Empty textarea while being requested. Repopulate in catch
this.resizeTextarea();
this.stopPolling();
......@@ -184,13 +189,25 @@ Please check your network connection and try again.`;
this.toggleIssueState();
}
},
enableButton() {
this.isSubmitting = false;
},
toggleIssueState() {
this.issueState = this.isIssueOpen ? constants.CLOSED : constants.REOPENED;
// This is out of scope for the Notes Vue component.
// It was the shortest path to update the issue state and relevant places.
const btnClass = this.isIssueOpen ? 'btn-reopen' : 'btn-close';
$(`.js-btn-issue-action.${btnClass}:visible`).trigger('click');
if (this.isIssueOpen) {
this.closeIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
Flash(__('Something went wrong while closing the issue. Please try again later'));
});
} else {
this.reopenIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
Flash(__('Something went wrong while reopening the issue. Please try again later'));
});
}
},
discard(shouldClear = true) {
// `blur` is needed to clear slash commands autocomplete cache if event fired.
......@@ -368,15 +385,19 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</li>
</ul>
</div>
<button
type="button"
@click="handleSave(true)"
<loading-button
v-if="canUpdateIssue"
:class="actionButtonClassNames"
:loading="isSubmitting"
@click="handleSave(true)"
:container-class="[
actionButtonClassNames,
'btn btn-comment btn-comment-and-close js-action-button'
]"
:disabled="isSubmitting"
class="btn btn-comment btn-comment-and-close js-action-button">
{{ issueActionButtonTitle }}
</button>
:label="issueActionButtonTitle"
/>
<button
type="button"
v-if="note.length"
......
......@@ -28,6 +28,8 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
notesPath: notesDataset.notesPath,
markdownDocsPath: notesDataset.markdownDocsPath,
quickActionsDocsPath: notesDataset.quickActionsDocsPath,
closeIssuePath: notesDataset.closeIssuePath,
reopenIssuePath: notesDataset.reopenIssuePath,
},
};
},
......
......@@ -32,4 +32,7 @@ export default {
toggleAward(endpoint, data) {
return Vue.http.post(endpoint, data, { emulateJSON: true });
},
toggleIssueState(endpoint, data) {
return Vue.http.put(endpoint, data);
},
};
......@@ -61,6 +61,39 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES);
export const closeIssue = ({ commit, dispatch, state }) => service
.toggleIssueState(state.notesData.closeIssuePath)
.then(res => res.json())
.then((data) => {
commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data);
});
export const reopenIssue = ({ commit, dispatch, state }) => service
.toggleIssueState(state.notesData.reopenIssuePath)
.then(res => res.json())
.then((data) => {
commit(types.REOPEN_ISSUE);
dispatch('emitStateChangedEvent', data);
});
export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: {
data,
isClosed: getters.issueState === constants.CLOSED,
} });
document.dispatchEvent(event);
};
export const toggleIssueLocalState = ({ commit }, newState) => {
if (newState === constants.CLOSED) {
commit(types.CLOSE_ISSUE);
} else if (newState === constants.REOPENED) {
commit(types.REOPEN_ISSUE);
}
};
export const saveNote = ({ commit, dispatch }, noteData) => {
const { note } = noteData.data.note;
let placeholderText = note;
......
......@@ -8,6 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const issueState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
......
......@@ -12,3 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
......@@ -152,4 +152,12 @@ export default {
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
}
},
[types.CLOSE_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.CLOSED });
},
[types.REOPEN_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.REOPENED });
},
};
......@@ -39,7 +39,7 @@
required: false,
},
containerClass: {
type: String,
type: [String, Array, Object],
required: false,
default: 'btn btn-align-content',
},
......
......@@ -457,9 +457,11 @@ img.emoji {
.prepend-top-10 { margin-top: 10px; }
.prepend-top-15 { margin-top: 15px; }
.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-16 { margin-top: 16px; }
.prepend-top-20 { margin-top: 20px; }
.prepend-left-4 { margin-left: 4px; }
.prepend-left-5 { margin-left: 5px; }
.prepend-left-8 { margin-left: 8px; }
.prepend-left-10 { margin-left: 10px; }
.prepend-left-15 { margin-left: 15px; }
.prepend-left-default { margin-left: $gl-padding; }
......
......@@ -736,10 +736,6 @@
}
}
.droplab-item-ignore {
pointer-events: none;
}
.pika-single.animate-picker.is-bound,
.pika-single.animate-picker.is-bound.is-hidden {
/*
......
......@@ -182,6 +182,7 @@ label {
.help-block {
margin-bottom: 0;
margin-top: #{$grid-size / 2};
}
.gl-field-error {
......
......@@ -201,11 +201,6 @@ ul.related-merge-requests > li {
}
.create-mr-dropdown-wrap {
.branch-message,
.ref-message {
display: none;
}
.ref::selection {
color: $placeholder-text-color;
}
......@@ -236,6 +231,17 @@ ul.related-merge-requests > li {
transform: translateY(0);
display: none;
margin-top: 4px;
// override dropdown item styles
.btn.btn-success {
@include btn-default;
@include btn-green;
border-style: solid;
border-width: 1px;
line-height: $line-height-base;
width: auto;
}
}
.create-merge-request-dropdown-toggle {
......@@ -245,66 +251,6 @@ ul.related-merge-requests > li {
margin-left: 0;
}
}
.droplab-item-ignore {
pointer-events: auto;
}
.create-item {
cursor: pointer;
margin: 0 1px;
&:hover,
&:focus {
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
}
}
li.divider {
margin: 8px 10px;
}
li:not(.divider) {
padding: 8px 9px;
&:last-child {
padding-bottom: 8px;
}
&.droplab-item-selected {
.icon-container {
i {
visibility: visible;
}
}
.description {
display: block;
}
}
&.droplab-item-ignore {
padding-top: 8px;
}
.icon-container {
float: left;
i {
visibility: hidden;
}
}
.description {
padding-left: 22px;
}
input,
span {
margin: 4px 0 0;
}
}
}
.discussion-reply-holder .note-edit-form {
......
......@@ -161,10 +161,12 @@ module MergeRequests
merge_request.title = "Resolve \"#{issue.title}\"" if issue.is_a?(Issue)
unless merge_request.title
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
return if merge_request.title.present?
if issue_iid.present?
merge_request.title = "Resolve #{issue_iid}"
merge_request.title += " \"#{branch_title}\"" unless branch_title.empty?
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
merge_request.title += " \"#{branch_title}\"" if branch_title.present?
end
end
......
......@@ -12,6 +12,8 @@
markdown_docs_path: help_page_path('user/markdown'),
quick_actions_docs_path: help_page_path('user/project/quick_actions'),
notes_path: notes_url,
close_issue_path: issue_path(@issue, issue: { state_event: :close }, format: 'json'),
reopen_issue_path: issue_path(@issue, issue: { state_event: :reopen }, format: 'json'),
last_fetched_at: Time.now.to_i,
noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
......@@ -21,30 +21,33 @@
%button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
= icon('caret-down')
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
%li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
.menu-item.droplab-item-ignore-hiding
.icon-container.droplab-item-ignore-hiding= icon('check')
.description.droplab-item-ignore-hiding Create merge request and branch
%li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
.menu-item.droplab-item-ignore-hiding
.icon-container.droplab-item-ignore-hiding= icon('check')
.description.droplab-item-ignore-hiding Create branch
%li.divider
%li.droplab-item-ignore
Branch name
%input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
%span.js-branch-message.branch-message.droplab-item-ignore
%li.droplab-item-ignore
Source (branch or tag)
%input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
%span.js-ref-message.ref-message.droplab-item-ignore
%li.droplab-item-ignore
%button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } }
Create merge request
.droplab-dropdown
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
- if can_create_merge_request
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
.menu-item
= icon('check', class: 'icon')
= _('Create merge request and branch')
%li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } }
.menu-item
= icon('check', class: 'icon')
= _('Create branch')
%li.divider.droplab-item-ignore
%li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16
.form-group
%label{ for: 'new-branch-name' }
= _('Branch name')
%input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
%span.js-branch-message.help-block
.form-group
%label{ for: 'source-name' }
= _('Source (branch or tag)')
%input#source-name.js-ref.ref.form-control{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
%span.js-ref-message.help-block
.form-group
%button.btn.btn-success.js-create-target{ type: 'button', data: { action: 'create-mr' } }
= _('Create merge request')
---
title: Fix close button on issues not working on mobile
merge_request:
author:
type: fixed
---
title: Cleanup new branch/merge request form in issues
merge_request: 16854
author:
type: fixed
import DropDown from '~/droplab/drop_down';
import utils from '~/droplab/utils';
import { SELECTED_CLASS, IGNORE_CLASS } from '~/droplab/constants';
import { SELECTED_CLASS } from '~/droplab/constants';
describe('DropDown', function () {
describe('DropLab DropDown', function () {
describe('class constructor', function () {
beforeEach(function () {
spyOn(DropDown.prototype, 'getItems');
......@@ -128,93 +128,131 @@ describe('DropDown', function () {
beforeEach(function () {
this.classList = jasmine.createSpyObj('classList', ['contains']);
this.list = { dispatchEvent: () => {} };
this.dropdown = { hide: () => {}, list: this.list, addSelectedClass: () => {} };
this.event = { preventDefault: () => {}, target: { classList: this.classList } };
this.dropdown = {
hideOnClick: true,
hide: () => {},
list: this.list,
addSelectedClass: () => {},
};
this.event = {
preventDefault: () => {},
target: {
classList: this.classList,
closest: () => null,
},
};
this.customEvent = {};
this.closestElement = {};
this.dummyListItem = document.createElement('li');
spyOn(this.event.target, 'closest').and.callFake((selector) => {
if (selector === 'li') {
return this.dummyListItem;
}
return null;
});
spyOn(this.dropdown, 'hide');
spyOn(this.dropdown, 'addSelectedClass');
spyOn(this.list, 'dispatchEvent');
spyOn(this.event, 'preventDefault');
spyOn(window, 'CustomEvent').and.returnValue(this.customEvent);
spyOn(utils, 'closest').and.returnValues(this.closestElement, undefined);
this.classList.contains.and.returnValue(false);
});
it('should call event.target.closest', function () {
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
});
it('should call utils.closest', function () {
expect(utils.closest).toHaveBeenCalledWith(this.event.target, 'LI');
expect(this.event.target.closest).toHaveBeenCalledWith('.droplab-item-ignore');
expect(this.event.target.closest).toHaveBeenCalledWith('li');
});
it('should call addSelectedClass', function () {
expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.closestElement);
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
expect(this.dropdown.addSelectedClass).toHaveBeenCalledWith(this.dummyListItem);
});
it('should call .preventDefault', function () {
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
expect(this.event.preventDefault).toHaveBeenCalled();
});
it('should call .hide', function () {
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
expect(this.dropdown.hide).toHaveBeenCalled();
});
it('should construct CustomEvent', function () {
expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object));
});
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
it('should call .classList.contains checking for IGNORE_CLASS', function () {
expect(this.classList.contains).toHaveBeenCalledWith(IGNORE_CLASS);
expect(window.CustomEvent).toHaveBeenCalledWith('click.dl', jasmine.any(Object));
});
it('should call .dispatchEvent with the customEvent', function () {
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
expect(this.list.dispatchEvent).toHaveBeenCalledWith(this.customEvent);
});
describe('if the target is a UL element', function () {
beforeEach(function () {
this.event = { preventDefault: () => {}, target: { tagName: 'UL', classList: this.classList } };
spyOn(this.event, 'preventDefault');
utils.closest.calls.reset();
this.event.target = document.createElement('ul');
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
spyOn(this.event.target, 'closest');
});
it('should return immediately', function () {
expect(utils.closest).not.toHaveBeenCalled();
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
expect(this.event.target.closest).not.toHaveBeenCalled();
expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled();
});
});
describe('if the target has the IGNORE_CLASS class', function () {
describe('if the target has the droplab-item-ignore class', function () {
beforeEach(function () {
this.event = { preventDefault: () => {}, target: { tagName: 'LI', classList: this.classList } };
this.ignoredButton = document.createElement('button');
this.ignoredButton.classList.add('droplab-item-ignore');
this.event.target = this.ignoredButton;
spyOn(this.event, 'preventDefault');
this.classList.contains.and.returnValue(true);
utils.closest.calls.reset();
spyOn(this.ignoredButton, 'closest').and.callThrough();
});
it('does not select element', function () {
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
});
it('should return immediately', function () {
expect(utils.closest).not.toHaveBeenCalled();
expect(this.ignoredButton.closest.calls.count()).toBe(1);
expect(this.ignoredButton.closest).toHaveBeenCalledWith('.droplab-item-ignore');
expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled();
});
});
describe('if no selected element exists', function () {
beforeEach(function () {
this.event.preventDefault.calls.reset();
this.clickEvent = DropDown.prototype.clickEvent.call(this.dropdown, this.event);
});
it('should return undefined', function () {
expect(this.clickEvent).toBe(undefined);
this.dummyListItem = null;
});
it('should return before .preventDefault is called', function () {
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
expect(this.event.preventDefault).not.toHaveBeenCalled();
expect(this.dropdown.addSelectedClass).not.toHaveBeenCalled();
});
});
describe('if hideOnClick is false', () => {
beforeEach(function () {
this.dropdown.hideOnClick = false;
this.dropdown.hide.calls.reset();
});
it('should not call .hide', function () {
DropDown.prototype.clickEvent.call(this.dropdown, this.event);
expect(this.dropdown.hide).not.toHaveBeenCalled();
});
});
});
......@@ -278,20 +316,23 @@ describe('DropDown', function () {
describe('addEvents', function () {
beforeEach(function () {
this.list = { addEventListener: () => {} };
this.list = {
addEventListener: () => {},
querySelectorAll: () => [],
};
this.dropdown = {
list: this.list,
clickEvent: () => {},
closeDropdown: () => {},
eventWrapper: {},
};
});
it('should call .addEventListener', function () {
spyOn(this.list, 'addEventListener');
DropDown.prototype.addEvents.call(this.dropdown);
});
it('should call .addEventListener', function () {
expect(this.list.addEventListener).toHaveBeenCalledWith('click', jasmine.any(Function));
expect(this.list.addEventListener).toHaveBeenCalledWith('keyup', jasmine.any(Function));
});
......
// eslint-disable-next-line import/prefer-default-export
export const resetStore = (store) => {
store.replaceState({
notes: [],
targetNoteHash: null,
lastFetchedAt: null,
notesData: {},
userData: {},
noteableData: {},
});
};
......@@ -7,6 +7,8 @@ export const notesDataMock = {
notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes',
quickActionsDocsPath: '/help/user/project/quick_actions',
registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane',
closeIssuePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close',
reopenIssuePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen',
};
export const userDataMock = {
......
import Vue from 'vue';
import _ from 'underscore';
import * as actions from '~/notes/stores/actions';
import store from '~/notes/stores';
import testAction from '../../helpers/vuex_action_helper';
import { resetStore } from '../helpers';
import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => {
afterEach(() => {
resetStore(store);
});
describe('setNotesData', () => {
it('should set received notes data', (done) => {
testAction(actions.setNotesData, null, { notesData: {} }, [
......@@ -58,4 +66,67 @@ describe('Actions Notes Store', () => {
], done);
});
});
describe('async methods', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
describe('closeIssue', () => {
it('sets state as closed', (done) => {
store.dispatch('closeIssue', { notesData: { closeIssuePath: '' } })
.then(() => {
expect(store.state.noteableData.state).toEqual('closed');
done();
})
.catch(done.fail);
});
});
describe('reopenIssue', () => {
it('sets state as reopened', (done) => {
store.dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } })
.then(() => {
expect(store.state.noteableData.state).toEqual('reopened');
done();
})
.catch(done.fail);
});
});
});
describe('emitStateChangedEvent', () => {
it('emits an event on the document', () => {
document.addEventListener('issuable_vue_app:change', (event) => {
expect(event.detail.data).toEqual({ id: '1', state: 'closed' });
expect(event.detail.isClosed).toEqual(false);
});
store.dispatch('emitStateChangedEvent', { id: '1', state: 'closed' });
});
});
describe('toggleIssueLocalState', () => {
it('sets issue state as closed', (done) => {
testAction(actions.toggleIssueLocalState, 'closed', {}, [
{ type: 'CLOSE_ISSUE', payload: 'closed' },
], done);
});
it('sets issue state as reopened', (done) => {
testAction(actions.toggleIssueLocalState, 'reopened', {}, [
{ type: 'REOPEN_ISSUE', payload: 'reopened' },
], done);
});
});
});
......@@ -55,4 +55,10 @@ describe('Getters Notes Store', () => {
expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]);
});
});
describe('issueState', () => {
it('should return the issue state', () => {
expect(getters.issueState(state)).toEqual(noteableDataMock.state);
});
});
});
......@@ -286,33 +286,43 @@ describe MergeRequests::BuildService do
end
end
context 'branch starts with JIRA-formatted external issue IID' do
let(:source_branch) { 'EXMPL-12345' }
describe 'with JIRA enabled' do
before do
allow(project).to receive(:external_issue_tracker).and_return(true)
allow(project).to receive(:issues_enabled?).and_return(false)
allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern)
end
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('Resolve EXMPL-12345')
end
context 'branch does not start with JIRA-formatted external issue IID' do
let(:source_branch) { 'test-branch' }
it 'appends the closes text' do
expect(merge_request.description).to eq('Closes EXMPL-12345')
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('Test branch')
end
end
context 'followed by hyphenated text' do
let(:source_branch) { 'EXMPL-12345-fix-issue' }
context 'branch starts with JIRA-formatted external issue IID' do
let(:source_branch) { 'EXMPL-12345' }
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('Resolve EXMPL-12345 "Fix issue"')
expect(merge_request.title).to eq('Resolve EXMPL-12345')
end
it 'appends the closes text' do
expect(merge_request.description).to eq('Closes EXMPL-12345')
end
context 'followed by hyphenated text' do
let(:source_branch) { 'EXMPL-12345-fix-issue' }
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('Resolve EXMPL-12345 "Fix issue"')
end
it 'appends the closes text' do
expect(merge_request.description).to eq('Closes EXMPL-12345')
end
end
end
end
end
......
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