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