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,6 +24,51 @@ export default class Issue { ...@@ -24,6 +24,51 @@ export default class Issue {
if (Issue.createMrDropdownWrap) { if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(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() { initIssueBtnEventListeners() {
...@@ -44,34 +89,8 @@ export default class Issue { ...@@ -44,34 +89,8 @@ export default class Issue {
url = $button.attr('href'); url = $button.attr('href');
return axios.put(url) return axios.put(url)
.then(({ data }) => { .then(({ data }) => {
const isClosedBadge = $('div.status-box-issue-closed'); const isClosed = $button.hasClass('btn-close');
const isOpenBadge = $('div.status-box-open'); this.updateTopState(isClosed, data);
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);
}
}) })
.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')
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } } .droplab-dropdown
- if can_create_merge_request %ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
%li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } } - if can_create_merge_request
.menu-item.droplab-item-ignore-hiding %li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', text: _('Create merge request') } }
.icon-container.droplab-item-ignore-hiding= icon('check') .menu-item
.description.droplab-item-ignore-hiding Create merge request and branch = icon('check', class: 'icon')
= _('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 %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } }
.icon-container.droplab-item-ignore-hiding= icon('check') .menu-item
.description.droplab-item-ignore-hiding Create branch = icon('check', class: 'icon')
%li.divider = _('Create branch')
%li.divider.droplab-item-ignore
%li.droplab-item-ignore
Branch name %li.droplab-item-ignore.prepend-left-8.append-right-8.prepend-top-16
%input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" } .form-group
%span.js-branch-message.branch-message.droplab-item-ignore %label{ for: 'new-branch-name' }
= _('Branch name')
%li.droplab-item-ignore %input#new-branch-name.js-branch-name.form-control{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
Source (branch or tag) %span.js-branch-message.help-block
%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 .form-group
%label{ for: 'source-name' }
%li.droplab-item-ignore = _('Source (branch or tag)')
%button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } } %input#source-name.js-ref.ref.form-control{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
Create merge request %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 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,33 +286,43 @@ describe MergeRequests::BuildService do ...@@ -286,33 +286,43 @@ 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
it 'sets the title to the humanized branch title' do context 'branch does not start with JIRA-formatted external issue IID' do
expect(merge_request.title).to eq('Resolve EXMPL-12345') let(:source_branch) { 'test-branch' }
end
it 'appends the closes text' do it 'sets the title to the humanized branch title' do
expect(merge_request.description).to eq('Closes EXMPL-12345') expect(merge_request.title).to eq('Test branch')
end
end end
context 'followed by hyphenated text' do context 'branch starts with JIRA-formatted external issue IID' do
let(:source_branch) { 'EXMPL-12345-fix-issue' } 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 "Fix issue"') expect(merge_request.title).to eq('Resolve EXMPL-12345')
end end
it 'appends the closes text' do it 'appends the closes text' do
expect(merge_request.description).to eq('Closes EXMPL-12345') expect(merge_request.description).to eq('Closes EXMPL-12345')
end 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 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