Commit b6238076 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas
Browse files

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

......@@ -349,6 +349,8 @@ group :development, :test do
gem 'activerecord_sane_schema_dumper', '0.2'
gem 'stackprof', '~> 0.2.10', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false
end
group :test do
......
......@@ -723,7 +723,7 @@ GEM
retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.2.0)
rouge (2.2.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
......@@ -833,6 +833,7 @@ GEM
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
simple_po_parser (1.1.2)
simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
......@@ -1145,6 +1146,7 @@ DEPENDENCIES
sidekiq (~> 5.0)
sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4)
simple_po_parser (~> 1.1.2)
simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1)
......
......@@ -2,17 +2,17 @@
import AccessorUtilities from './lib/utils/accessor';
window.Autosave = (function() {
function Autosave(field, key) {
function Autosave(field, key, resource) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource;
if (key.join != null) {
key = key.join("/");
key = key.join('/');
}
this.key = "autosave/" + key;
this.field.data("autosave", this);
this.key = 'autosave/' + key;
this.field.data('autosave', this);
this.restore();
this.field.on("input", (function(_this) {
this.field.on('input', (function(_this) {
return function() {
return _this.save();
};
......@@ -29,7 +29,17 @@ window.Autosave = (function() {
if ((text != null ? text.length : void 0) > 0) {
this.field.val(text);
}
return this.field.trigger("input");
if (!this.resource && this.resource !== 'issue') {
this.field.trigger('input');
} else {
// v-model does not update with jQuery trigger
// https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
const event = new Event('change', { bubbles: true, cancelable: false });
const field = this.field.get(0);
if (field) {
field.dispatchEvent(event);
}
}
};
Autosave.prototype.save = function() {
......
......@@ -109,6 +109,7 @@ class AwardsHandler {
}
$thumbsBtn.toggleClass('disabled', $userAuthored);
$thumbsBtn.prop('disabled', $userAuthored);
}
// Create the emoji menu with the first category of emojis.
......@@ -234,14 +235,33 @@ class AwardsHandler {
}
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length;
if (gl.utils.isInIssuePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
$('.emoji-menu').removeClass('is-visible');
$('.js-add-award.is-active').removeClass('is-active');
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
awardName: emoji,
noteId: id,
},
});
document.querySelector('.js-vue-notes-event').dispatchEvent(toggleAwardEvent);
}
const normalizedEmoji = this.emoji.normalizeEmojiName(emoji);
const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent();
this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => {
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined;
});
$('.emoji-menu').removeClass('is-visible');
$('.js-add-award.is-active').removeClass('is-active');
return $('.js-add-award.is-active').removeClass('is-active');
}
addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) {
......@@ -268,6 +288,14 @@ class AwardsHandler {
}
getVotesBlock() {
if (gl.utils.isInIssuePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) {
return $el;
}
}
const currentBlock = $('.js-awards-block.current');
let resultantVotesBlock = currentBlock;
if (currentBlock.length === 0) {
......
......@@ -44,7 +44,10 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => {
if (!$submitButton.attr('disabled')) {
$submitButton.trigger('click', [e]);
$submitButton.disable();
if (!gl.utils.isInIssuePage()) {
$submitButton.disable();
}
}
});
......
......@@ -99,7 +99,7 @@ import initChangesDropdown from './init_changes_dropdown';
path = page.split(':');
shortcut_handler = null;
$('.js-gfm-input').each((i, el) => {
$('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = gl.utils.convertPermissionToBoolean(el.dataset.supportsAutocomplete);
gfm.setup($(el), {
......@@ -172,7 +172,6 @@ import initChangesDropdown from './init_changes_dropdown';
shortcut_handler = new ShortcutsIssuable();
new ZenMode();
initIssuableSidebar();
initNotes();
break;
case 'dashboard:milestones:index':
new ProjectSelect();
......
......@@ -128,7 +128,7 @@ window.DropzoneInput = (function() {
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
$cancelButton.on('click', (e) => {
const target = e.target.closest('form').querySelector('.div-dropzone');
const target = e.target.closest('.js-main-target-form').querySelector('.div-dropzone');
e.preventDefault();
e.stopPropagation();
......@@ -140,7 +140,7 @@ window.DropzoneInput = (function() {
// and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it.
$retryLink.on('click', (e) => {
const dropzoneInstance = Dropzone.forElement(e.target.closest('form').querySelector('.div-dropzone'));
const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone'));
const failedFiles = dropzoneInstance.files;
e.preventDefault();
......
......@@ -12,6 +12,7 @@ let sidebar;
export const mousePos = [];
export const setSidebar = (el) => { sidebar = el; };
export const getOpenMenu = () => currentOpenMenu;
export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; };
export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
......@@ -141,6 +142,14 @@ export const documentMouseMove = (e) => {
if (mousePos.length > 6) mousePos.shift();
};
export const subItemsMouseLeave = (relatedTarget) => {
clearTimeout(timeoutId);
if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
hideMenu(currentOpenMenu);
}
};
export default () => {
sidebar = document.querySelector('.nav-sidebar');
......@@ -162,10 +171,7 @@ export default () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (subItems) {
subItems.addEventListener('mouseleave', () => {
clearTimeout(timeoutId);
hideMenu(currentOpenMenu);
});
subItems.addEventListener('mouseleave', e => subItemsMouseLeave(e.relatedTarget));
}
el.addEventListener('mouseenter', e => mouseEnterTopItems(e.currentTarget));
......
......@@ -42,7 +42,7 @@ class Issue {
initIssueBtnEventListeners() {
const issueFailMessage = 'Unable to update this issue at this time.';
return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => {
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();
......@@ -66,12 +66,11 @@ class Issue {
const projectIssuesCounter = $('.issue_counter');
if ('id' in data) {
$(document).trigger('issuable:change');
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.text().replace(/[^\d]/, ''));
......@@ -121,7 +120,7 @@ class Issue {
static submitNoteForm(form) {
var noteText;
noteText = form.find("textarea.js-note-text").val();
if (noteText.trim().length > 0) {
if (noteText && noteText.trim().length > 0) {
return form.submit();
}
}
......
......@@ -80,11 +80,11 @@ export default {
type: Boolean,
required: true,
},
markdownPreviewUrl: {
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocs: {
markdownDocsPath: {
type: String,
required: true,
},
......@@ -96,7 +96,7 @@ export default {
type: String,
required: true,
},
projectsAutocompleteUrl: {
projectsAutocompletePath: {
type: String,
required: true,
},
......@@ -242,11 +242,11 @@ export default {
:can-move="canMove"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
:markdown-docs="markdownDocs"
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:projects-autocomplete-url="projectsAutocompleteUrl"
:projects-autocomplete-path="projectsAutocompletePath"
/>
<div v-else>
<title-component
......
......@@ -10,11 +10,11 @@
type: Object,
required: true,
},
markdownPreviewUrl: {
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocs: {
markdownDocsPath: {
type: String,
required: true,
},
......@@ -36,8 +36,8 @@
Description
</label>
<markdown-field
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs">
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath">
<textarea
id="issue-description"
class="note-textarea js-gfm-input js-autosize markdown-area"
......
......@@ -10,7 +10,7 @@
type: Object,
required: true,
},
projectsAutocompleteUrl: {
projectsAutocompletePath: {
type: String,
required: true,
},
......@@ -20,7 +20,7 @@
$moveDropdown.select2({
ajax: {
url: this.projectsAutocompleteUrl,
url: this.projectsAutocompletePath,
quietMillis: 125,
data(term, page, context) {
return {
......
......@@ -26,11 +26,11 @@
required: false,
default: () => [],
},
markdownPreviewUrl: {
markdownPreviewPath: {
type: String,
required: true,
},
markdownDocs: {
markdownDocsPath: {
type: String,
required: true,
},
......@@ -42,7 +42,7 @@
type: String,
required: true,
},
projectsAutocompleteUrl: {
projectsAutocompletePath: {
type: String,
required: true,
},
......@@ -89,14 +89,14 @@
</div>
<description-field
:form-state="formState"
:markdown-preview-url="markdownPreviewUrl"
:markdown-docs="markdownDocs" />
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" />
<confidential-checkbox
:form-state="formState" />
<project-move
v-if="canMove"
:form-state="formState"
:projects-autocomplete-url="projectsAutocompleteUrl" />
:projects-autocomplete-path="projectsAutocompletePath" />
<edit-actions
:form-state="formState"
:can-destroy="canDestroy" />
......
......@@ -37,11 +37,11 @@ document.addEventListener('DOMContentLoaded', () => {
initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates,
isConfidential: this.isConfidential,
markdownPreviewUrl: this.markdownPreviewUrl,
markdownDocs: this.markdownDocs,
markdownPreviewPath: this.markdownPreviewPath,
markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath,
projectNamespace: this.projectNamespace,
projectsAutocompleteUrl: this.projectsAutocompleteUrl,
projectsAutocompletePath: this.projectsAutocompletePath,
updatedAt: this.updatedAt,
updatedByName: this.updatedByName,
updatedByPath: this.updatedByPath,
......
......@@ -27,6 +27,13 @@
}
};
w.gl.utils.isInIssuePage = () => {
const page = gl.utils.getPagePath(1);
const action = gl.utils.getPagePath(2);
return page === 'issues' && action === 'show';
};
w.gl.utils.ajaxGet = function(url) {
return $.ajax({
type: "GET",
......@@ -167,11 +174,12 @@
};
gl.utils.scrollToElement = function($el) {
var top = $el.offset().top;
gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height();
const top = $el.offset().top;
const mrTabsHeight = $('.merge-request-tabs').height() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0;
return $('body, html').animate({
scrollTop: top - (gl.mrTabsHeight)
scrollTop: top - mrTabsHeight - headerHeight,
}, 200);
};
......
<script>
/* global Flash, Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue';
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
name: 'issueCommentForm',
data() {
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.getIssueData.state,
isSubmitting: false,
isSubmitButtonDisabled: true,
};
},
components: {
confidentialIssue,
issueNoteSignedOutWidget,
markdownField,
userAvatarLink,
},
watch: {
note(newNote) {
this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
},
isSubmitting(newValue) {
this.setIsSubmitButtonDisabled(this.note, newValue);
},
},
computed: {
...mapGetters([
'getCurrentUserLastNote',
'getUserData',
'getIssueData',
'getNotesData',
]),
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
},
isIssueOpen() {
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
},
issueActionButtonTitle() {
if (this.note.length) {
const actionText = this.isIssueOpen ? 'close' : 'reopen';
return this.noteType === constants.COMMENT ? `Comment & ${actionText} issue` : `Start discussion & ${actionText} issue`;
}
return this.isIssueOpen ? 'Close issue' : 'Reopen issue';
},
actionButtonClassNames() {
return {
'btn-reopen': !this.isIssueOpen,
'btn-close': this.isIssueOpen,
'js-note-target-close': this.isIssueOpen,
'js-note-target-reopen': !this.isIssueOpen,
};
},
markdownDocsPath() {
return this.getNotesData.markdownDocsPath;
},
quickActionsDocsPath() {
return this.getNotesData.quickActionsDocsPath;
},
markdownPreviewPath() {
return this.getIssueData.preview_note_path;
},
author() {
return this.getUserData;
},
canUpdateIssue() {
return this.getIssueData.current_user.can_update;
},
endpoint() {
return this.getIssueData.create_note_path;
},
isConfidentialIssue() {
return this.getIssueData.confidential;
},
},
methods: {
...mapActions([
'saveNote',
'removePlaceholderNotes',
]),
setIsSubmitButtonDisabled(note, isSubmitting) {
if (!_.isEmpty(note) && !isSubmitting) {
this.isSubmitButtonDisabled = false;
} else {
this.isSubmitButtonDisabled = true;
}
},
handleSave(withIssueAction) {
if (this.note.length) {
const noteData = {
endpoint: this.endpoint,
flashContainer: this.$el,
data: {
note: {
noteable_type: constants.NOTEABLE_TYPE,
noteable_id: this.getIssueData.id,
note: this.note,
},
},
};
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.saveNote(noteData)
.then((res) => {
this.isSubmitting = false;
if (res.errors) {
if (res.errors.commands_only) {
this.discard();
} else {
Flash(
'Something went wrong while adding your comment. Please try again.',
'alert',
$(this.$refs.commentForm),
);
}
} else {
this.discard();
}
if (withIssueAction) {
this.toggleIssueState();
}
})
.catch(() => {
this.isSubmitting = false;
this.discard(false);
const msg = 'Your comment could not be submitted! Please check your network connection and try again.';
Flash(msg, 'alert', $(this.$el));
this.note = noteData.data.note.note; // Restore textarea content.
this.removePlaceholderNotes();
});
} else {
this.toggleIssueState();
}
},
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');