Commit 771cc617 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-03-16' into 'master'

CE upstream - 2018-03-16 21:24 UTC

See merge request gitlab-org/gitlab-ee!5004
parents e58fce3f de13c3a5
......@@ -315,7 +315,7 @@ stages:
##
# Trigger a package build in omnibus-gitlab repository
#
package-qa:
package-and-qa:
<<: *dedicated-runner
image: ruby:2.4-alpine
before_script: []
......
......@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores';
export default function initMrNotes() {
new Vue({ // eslint-disable-line
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions',
components: {
notesApp,
},
data() {
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
return {
noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData),
......@@ -28,7 +30,8 @@ export default function initMrNotes() {
},
});
new Vue({ // eslint-disable-line
// eslint-disable-next-line no-new
new Vue({
el: '#js-vue-discussion-counter',
components: {
discussionCounter,
......
This diff is collapsed.
<script>
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import {
capitalizeFirstCharacter,
convertToCamelCase,
} from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import issuableStateMixin from '../mixins/issuable_state';
export default {
export default {
name: 'CommentForm',
components: {
issueWarning,
......@@ -28,9 +31,7 @@
userAvatarLink,
loadingButton,
},
mixins: [
issuableStateMixin,
],
mixins: [issuableStateMixin],
props: {
noteableType: {
type: String,
......@@ -53,9 +54,7 @@
'getNotesData',
'openState',
]),
...mapState([
'isToggleStateButtonLoading',
]),
...mapState(['isToggleStateButtonLoading']),
noteableDisplayName() {
return this.noteableType.replace(/_/g, ' ');
},
......@@ -63,10 +62,15 @@
return this.getUserData.id;
},
commentButtonTitle() {
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
return this.noteType === constants.COMMENT
? 'Comment'
: 'Start discussion';
},
isOpen() {
return this.openState === constants.OPENED || this.openState === constants.REOPENED;
return (
this.openState === constants.OPENED ||
this.openState === constants.REOPENED
);
},
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
......@@ -75,23 +79,17 @@
const openOrClose = this.isOpen ? 'close' : 'reopen';
if (this.note.length) {
return sprintf(
__('%{actionText} & %{openOrClose} %{noteable}'),
{
return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), {
actionText: this.commentButtonTitle,
openOrClose,
noteable: this.noteableDisplayName,
},
);
});
}
return sprintf(
__('%{openOrClose} %{noteable}'),
{
return sprintf(__('%{openOrClose} %{noteable}'), {
openOrClose: capitalizeFirstCharacter(openOrClose),
noteable: this.noteableDisplayName,
},
);
});
},
actionButtonClassNames() {
return {
......@@ -131,7 +129,9 @@
mounted() {
// jQuery is needed here because it is a custom event being dispatched with jQuery.
$(document).on('issuable:change', (e, isClosed) => {
this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
this.toggleIssueLocalState(
isClosed ? constants.CLOSED : constants.REOPENED,
);
});
this.initAutoSave();
......@@ -180,7 +180,7 @@
this.stopPolling();
this.saveNote(noteData)
.then((res) => {
.then(res => {
this.enableButton();
this.restartPolling();
......@@ -205,8 +205,7 @@
.catch(() => {
this.enableButton();
this.discard(false);
const msg =
`Your comment could not be submitted!
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.
......@@ -228,7 +227,9 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
__('Something went wrong while closing the %{issuable}. Please try again later'),
__(
'Something went wrong while closing the %{issuable}. Please try again later',
),
{ issuable: this.noteableDisplayName },
),
);
......@@ -241,7 +242,9 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false);
Flash(
sprintf(
__('Something went wrong while reopening the %{issuable}. Please try again later'),
__(
'Something went wrong while reopening the %{issuable}. Please try again later',
),
{ issuable: this.noteableDisplayName },
),
);
......@@ -278,12 +281,15 @@ Please check your network connection and try again.`;
},
initAutoSave() {
if (this.isLoggedIn) {
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
this.autosave = new Autosave(
$(this.$refs.textarea),
['Note', noteableType, this.getNoteableData.id],
const noteableType = capitalizeFirstCharacter(
convertToCamelCase(this.noteableType),
);
this.autosave = new Autosave($(this.$refs.textarea), [
'Note',
noteableType,
this.getNoteableData.id,
]);
}
},
initTaskList() {
......@@ -299,7 +305,7 @@ Please check your network connection and try again.`;
});
},
},
};
};
</script>
<template>
......
<script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
export default {
components: {
ClipboardButton,
Icon,
......@@ -18,7 +18,7 @@
return this.diffFile.discussionPath ? 'a' : 'span';
},
},
};
};
</script>
<template>
......
<script>
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue';
import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue';
export default {
export default {
components: {
DiffFileHeader,
},
......@@ -37,7 +37,11 @@
if (this.isImageDiff) {
const canCreateNote = false;
const renderCommentBadge = true;
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
imageDiffHelper.initImageDiff(
this.$refs.fileHolder,
canCreateNote,
renderCommentBadge,
);
} else {
const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => {
......@@ -50,7 +54,7 @@
return html.outerHTML ? 'tr' : 'template';
},
},
};
};
</script>
<template>
......
<script>
import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip';
import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
export default {
directives: {
tooltip,
},
......@@ -49,7 +49,9 @@
},
methods: {
jumpToFirstDiscussion() {
const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`);
const el = document.querySelector(
`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
);
const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') {
......@@ -61,7 +63,7 @@
}
},
},
};
};
</script>
<template>
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Issuable from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import Issuable from '~/vue_shared/mixins/issuable';
export default {
export default {
components: {
Icon,
},
mixins: [
Issuable,
],
};
mixins: [Issuable],
};
</script>
<template>
......
<script>
import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
export default {
name: 'NoteActions',
directives: {
tooltip,
......@@ -70,9 +70,7 @@
},
},
computed: {
...mapGetters([
'getUserDataByProp',
]),
...mapGetters(['getUserDataByProp']),
shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
},
......@@ -115,7 +113,7 @@
this.$emit('handleResolve');
},
},
};
};
</script>
<template>
......
<script>
export default {
export default {
name: 'NoteAttachment',
props: {
attachment: {
......@@ -7,7 +7,7 @@
required: true,
},
},
};
};
</script>
<template>
......
<script>
import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
directives: {
tooltip,
},
......@@ -30,9 +30,7 @@
},
},
computed: {
...mapGetters([
'getUserData',
]),
...mapGetters(['getUserData']),
// `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
// [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
// This method will group emojis by their name as an Object. See below.
......@@ -79,9 +77,7 @@
this.emojiSmiley = emojiSmiley;
},
methods: {
...mapActions([
'toggleAwardRequest',
]),
...mapActions(['toggleAwardRequest']),
getAwardHTML(name) {
return glEmojiTag(name);
},
......@@ -96,30 +92,43 @@
const restrictedEmojis = ['thumbsup', 'thumbsdown'];
// Users can not add :+1: and :-1: to their own notes
if (this.getUserData.id === this.noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) {
if (
this.getUserData.id === this.noteAuthorId &&
restrictedEmojis.indexOf(awardName) > -1
) {
isAllowed = false;
}
return this.getUserData.id && isAllowed;
},
hasReactionByCurrentUser(awardList) {
return awardList.filter(award => award.user.id === this.getUserData.id).length;
return awardList.filter(award => award.user.id === this.getUserData.id)
.length;
},
awardTitle(awardsList) {
const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList);
const hasReactionByCurrentUser = this.hasReactionByCurrentUser(
awardsList,
);
const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
let awardList = awardsList;
// Filter myself from list if I am awarded.
if (hasReactionByCurrentUser) {
awardList = awardList.filter(award => award.user.id !== this.getUserData.id);
awardList = awardList.filter(
award => award.user.id !== this.getUserData.id,
);
}
// Get only 9-10 usernames to show in tooltip text.
const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name);
const namesToShow = awardList
.slice(0, TOOLTIP_NAME_COUNT)
.map(award => award.user.name);
// Get the remaining list to use in `and x more` text.
const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
const remainingAwardList = awardList.slice(
TOOLTIP_NAME_COUNT,
awardList.length,
);
// Add myself to the begining of the list so title will start with You.
if (hasReactionByCurrentUser) {
......@@ -130,14 +139,17 @@
// We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) {
title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`;
title = `${namesToShow.join(', ')}, and ${
remainingAwardList.length
} more.`;
} else if (namesToShow.length > 1) {
// Join all names with comma but not the last one, it will be added with and text.
title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
// If we have more than 2 users we need an extra comma before and text.
title += namesToShow.length > 2 ? ',' : '';
title += ` and ${namesToShow.slice(-1)}`; // Append and text
} else { // We have only 2 users so join them with and.
} else {
// We have only 2 users so join them with and.
title = namesToShow.join(' and ');
}
......@@ -169,11 +181,12 @@
awardName: parsedName,
};
this.toggleAwardRequest(data)
.catch(() => Flash('Something went wrong on our end.'));
this.toggleAwardRequest(data).catch(() =>
Flash('Something went wrong on our end.'),
);
},
},
};
};
</script>
<template>
......
<script>
import $ from 'jquery';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue';
import TaskList from '../../task_list';
import autosave from '../mixins/autosave';
import $ from 'jquery';
import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue';
import TaskList from '../../task_list';
import autosave from '../mixins/autosave';
export default {
export default {
components: {
noteEditedText,
noteAwardsList,
noteAttachment,
noteForm,
},
mixins: [
autosave,
],
mixins: [autosave],
props: {
note: {
type: Object,
......@@ -77,7 +75,7 @@
this.$emit('cancelFormEdition', shouldConfirm, isDirty);
},
},
};
};
</script>
<template>
......
<script>
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
export default {
name: 'EditedNoteText',
components: {
timeAgoTooltip,
......@@ -26,7 +26,7 @@
default: 'edited-text',
},
},
};
};
</script>
<template>
......
<script>
import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable';
export default {
export default {
name: 'IssueNoteForm',
components: {
issueWarning,
markdownField,
},
mixins: [
issuableStateMixin,
resolvable,
],
mixins: [issuableStateMixin, resolvable],
props: {
noteBody: {
type: String,
......@@ -69,7 +66,9 @@
return this.getNotesDataByProp('markdownDocsPath');
},
quickActionsDocsPath() {
return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
return !this.isEditing
? this.getNotesDataByProp('quickActionsDocsPath')
: undefined;
},
currentUserId() {
return this.getUserDataByProp('id');
......@@ -91,24 +90,29 @@
this.$refs.textarea.focus();
},
methods: {
...mapActions([
'toggleResolveNote',
]),
...mapActions(['toggleResolveNote']),
handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
this.$emit(
'handleFormUpdate',
this.updatedNoteBody,
this.$refs.editNoteForm,
() => {
this.isSubmitting = false;
if (shouldResolve) {
this.resolveHandler(beforeSubmitDiscussionState);
}
});
},
);
},
editMyLastNote() {
if (this.updatedNoteBody === '') {
const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody);
const lastNoteInDiscussion = this.getDiscussionLastNote(
this.updatedNoteBody,
);
if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', {
......@@ -119,10 +123,14 @@
},
cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed
this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody);
this.$emit(
'cancelFormEdition',
shouldConfirm,
this.noteBody !== this.updatedNoteBody,
);
},
},
};
};
</script>
<template>
......
<script>
import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default {
export default {
components: {
timeAgoTooltip,
},
......@@ -49,9 +49,7 @@
},
},
methods: {
...mapActions([
'setTargetNoteHash',
]),
...mapActions(['setTargetNoteHash']),
handleToggle() {
this.$emit('toggleHandler');
},
......@@ -59,7 +57,7 @@
this.setTargetNoteHash(this.noteTimestampLink);
},
},
};
};
</script>
<template>
......
<script>
import { mapGetters } from 'vuex';
import { mapGetters } from 'vuex';
export default {
export default {
computed: {
...mapGetters([
'getNotesDataByProp',
]),
...mapGetters(['getNotesDataByProp']),
registerLink() {
return this.getNotesDataByProp('registerPath');
},
......@@ -13,7 +11,7 @@
return this.getNotesDataByProp('newSessionPath');
},
},
};
};
</script>
<template>
......
<script>
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip';
import { scrollToElement } from '../../lib/utils/common_utils';
import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip';
import { scrollToElement } from '../../lib/utils/common_utils';
export default {
export default {
components: {
noteableNote,
diffWithNote,
......@@ -34,11 +34,7 @@
directives: {
tooltip,
},
mixins: [
autosave,
noteable,
resolvable,
],
mixins: [autosave, noteable, resolvable],
props: {
note: {
type: Object,
......@@ -99,7 +95,9 @@
return this.unresolvedDiscussions.length > 0;
},
wrapperComponent() {
return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div';
return this.discussion.diffDiscussion && this.discussion.diffFile
? diffWithNote
: 'div';
},
wrapperClass() {
return this.isDiffDiscussion ? '' : 'panel panel-default';
......@@ -151,8 +149,10 @@
},
cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) {
const msg = 'Are you sure you want to cancel creating this comment?';
// eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel creating this comment?')) {
if (!confirm(msg)) {
return;
}
}
......@@ -178,7 +178,7 @@
this.resetAutoSave();
callback();
})
.catch((err) => {
.catch(err => {
this.removePlaceholderNotes();
this.isReplying = true;
this.$nextTick(() => {
......@@ -204,7 +204,7 @@ Please check your network connection and try again.`;
}
},
},
};
};
</script>
<template>
......
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
import noteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue';
import noteBody from './note_body.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
export default {
export default {
components: {
userAvatarLink,
noteHeader,
noteActions,
noteBody,
},
mixins: [
noteable,
resolvable,
],
mixins: [noteable, resolvable],
props: {
note: {
type: Object,
......@@ -37,10 +34,7 @@
};
},
computed: {
...mapGetters([
'targetNoteHash',
'getUserData',
]),
...mapGetters(['targetNoteHash', 'getUserData']),
author() {
return this.note.author;
},
......@@ -53,7 +47,9 @@
};
},
canReportAsAbuse() {
return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
return (
this.note.report_abuse_path && this.author.id !== this.getUserData.id
);
},
noteAnchorId() {
return `note_${this.note.id}`;
......@@ -89,7 +85,9 @@
this.isDeleting = false;
})
.catch(() => {
Flash('Something went wrong while deleting your note. Please try again.');
Flash(
'Something went wrong while deleting your note. Please try again.',
);
this.isDeleting = false;
});
}
......@@ -120,7 +118,8 @@
this.isRequesting = false;
this.isEditing = true;
this.$nextTick(() => {
const msg = 'Something went wrong while editing your comment. Please try again.';
const msg =
'Something went wrong while editing your comment. Please try again.';
Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText);
callback();
......@@ -130,7 +129,8 @@
formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel editing this comment?')) return;
if (!confirm('Are you sure you want to cancel editing this comment?'))
return;
}
this.$refs.noteBody.resetAutoSave();
if (this.oldContent) {
......@@ -146,7 +146,7 @@
this.$refs.noteBody.$refs.noteForm.note.note = noteText;
},
},
};
};
</script>
<template>
......
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash';
import store from '../stores/';
import * as constants from '../constants';
import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default {
export default {
name: 'NotesApp',
components: {
noteableNote,
......@@ -47,16 +47,14 @@
};
},
computed: {
...mapGetters([
'notes',
'getNotesDataByProp',
'discussionCount',
]),
...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
noteableType() {
// FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
return this.noteableData.merge_params
? MERGE_REQUEST_NOTEABLE_TYPE
: ISSUE_NOTEABLE_TYPE;
},
allNotes() {
if (this.isLoading) {
......@@ -79,9 +77,11 @@
const parentElement = this.$el.parentElement;
if (parentElement &&
parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', (event) => {
if (
parentElement &&
parentElement.classList.contains('js-vue-notes-event')
) {
parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId });
});
......@@ -131,7 +131,9 @@
.then(() => this.checkLocationHash())
.catch(() => {
this.isLoading = false;
Flash('Something went wrong while fetching comments. Please try again.');
Flash(
'Something went wrong while fetching comments. Please try again.',
);
});
},
initPolling() {
......@@ -154,7 +156,7 @@
}
},
},
};
};
</script>
<template>
......
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
document.addEventListener(
'DOMContentLoaded',
() =>
new Vue({
el: '#js-vue-notes',
components: {
notesApp,
......@@ -9,13 +12,17 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const currentUserData = parsedUserData ? {
let currentUserData = {};
if (parsedUserData) {
currentUserData = {
id: parsedUserData.id,
name: parsedUserData.name,
username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path,
} : {};
};
}
return {
noteableData: JSON.parse(notesDataset.noteableData),
......@@ -32,4 +39,5 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
}));
}),
);
......@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
initAutoSave(noteableType) {
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]);
this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
'Note',
capitalizeFirstCharacter(noteableType),
this.note.id,
]);
},
resetAutoSave() {
this.autosave.reset();
......
......@@ -12,7 +12,8 @@ export default {
discussionResolved() {
const { notes, resolved } = this.note;
if (notes) { // Decide resolved state using store. Only valid for discussions.
if (notes) {
// Decide resolved state using store. Only valid for discussions.
return notes.every(note => note.resolved && !note.system);
}
......@@ -26,7 +27,9 @@ export default {
return __('Comment and resolve discussion');
}
return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
return this.discussionResolved
? __('Unresolve discussion')
: __('Resolve discussion');
},
},
methods: {
......@@ -42,7 +45,9 @@ export default {
})
.catch(() => {
this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.');
const msg = __(
'Something went wrong while resolving this discussion. Please try again.',
);
Flash(msg, 'alert', this.$el);
});
},
......
......@@ -22,7 +22,9 @@ export default {
},
toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
const method = isResolved
? UNRESOLVE_NOTE_METHOD_NAME
: RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint);
},
......
......@@ -12,47 +12,57 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
export const fetchNotes = ({ commit }, path) => service
export const setNotesData = ({ commit }, data) =>
commit(types.SET_NOTES_DATA, data);
export const setNoteableData = ({ commit }, data) =>
commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) =>
commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) =>
commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) =>
commit(types.SET_INITIAL_NOTES, data);
export const setTargetNoteHash = ({ commit }, data) =>
commit(types.SET_TARGET_NOTE_HASH, data);
export const toggleDiscussion = ({ commit }, data) =>
commit(types.TOGGLE_DISCUSSION, data);
export const fetchNotes = ({ commit }, path) =>
service
.fetchNotes(path)
.then(res => res.json())
.then((res) => {
.then(res => {
commit(types.SET_INITIAL_NOTES, res);
});
export const deleteNote = ({ commit }, note) => service
.deleteNote(note.path)
.then(() => {
export const deleteNote = ({ commit }, note) =>
service.deleteNote(note.path).then(() => {
commit(types.DELETE_NOTE, note);
});
export const updateNote = ({ commit }, { endpoint, note }) => service
export const updateNote = ({ commit }, { endpoint, note }) =>
service
.updateNote(endpoint, note)
.then(res => res.json())
.then((res) => {
.then(res => {
commit(types.UPDATE_NOTE, res);
});
export const replyToDiscussion = ({ commit }, { endpoint, data }) => service
export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
service
.replyToDiscussion(endpoint, data)
.then(res => res.json())
.then((res) => {
.then(res => {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
return res;
});
export const createNewNote = ({ commit }, { endpoint, data }) => service
export const createNewNote = ({ commit }, { endpoint, data }) =>
service
.createNewNote(endpoint, data)
.then(res => res.json())
.then((res) => {
.then(res => {
if (!res.errors) {
commit(types.ADD_NEW_NOTE, res);
}
......@@ -62,11 +72,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES);
export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service
export const toggleResolveNote = (
{ commit },
{ endpoint, isResolved, discussion },
) =>
service
.toggleResolveNote(endpoint, isResolved)
.then(res => res.json())
.then((res) => {
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
.then(res => {
const mutationType = discussion
? types.UPDATE_DISCUSSION
: types.UPDATE_NOTE;
commit(mutationType, res);
});
......@@ -76,7 +92,7 @@ export const closeIssue = ({ commit, dispatch, state }) => {
return service
.toggleIssueState(state.notesData.closePath)
.then(res => res.json())
.then((data) => {
.then(data => {
commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false);
......@@ -88,7 +104,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
return service
.toggleIssueState(state.notesData.reopenPath)
.then(res => res.json())
.then((data) => {
.then(data => {
commit(types.REOPEN_ISSUE);
dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false);
......@@ -99,10 +115,12 @@ export const toggleStateButtonLoading = ({ commit }, value) =>
commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: {
const event = new CustomEvent('issuable_vue_app:change', {
detail: {
data,
isClosed: getters.openState === constants.CLOSED,
} });
},
});
document.dispatchEvent(event);
};
......@@ -144,8 +162,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
}
return dispatch(methodToDispatch, noteData)
.then((res) => {
return dispatch(methodToDispatch, noteData).then(res => {
const { errors } = res;
const commandsChanges = res.commands_changes;
......@@ -161,8 +178,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const votesBlock = $('.js-awards-block').eq(0);
loadAwardsHandler()
.then((awardsHandler) => {
awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
.then(awardsHandler => {
awardsHandler.addAwardToEmojiBar(
votesBlock,
commandsChanges.emoji_award,
);
awardsHandler.scrollToAwards();
})
.catch(() => {
......@@ -174,7 +194,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
}
if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
if (
commandsChanges.spend_time != null ||
commandsChanges.time_estimate != null
) {
sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
}
}
......@@ -192,11 +215,17 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
if (resp.notes && resp.notes.length) {
const { notesById } = getters;
resp.notes.forEach((note) => {
resp.notes.forEach(note => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
} else if (
note.type === constants.DISCUSSION_NOTE ||
note.type === constants.DIFF_NOTE
) {
const discussion = utils.findNoteObjectById(
state.notes,
note.discussion_id,
);
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
......@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => {
resource: service,
method: 'poll',
data: state,
successCallback: resp => resp.json()
successCallback: resp =>
resp
.json()
.then(data => pollSuccessCallBack(data, commit, state, getters)),
errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
errorCallback: () =>
Flash('Something went wrong while fetching latest comments.'),
});
if (!Visibility.hidden()) {
......@@ -248,15 +280,22 @@ export const restartPolling = () => {
};
export const fetchData = ({ commit, state, getters }) => {
const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt };
const requestData = {
endpoint: state.notesData.notesPath,
lastFetchedAt: state.lastFetchedAt,
};
service.poll(requestData)
service
.poll(requestData)
.then(resp => resp.json)
.then(data => pollSuccessCallBack(data, commit, state, getters))
.catch(() => Flash('Something went wrong while fetching latest comments.'));
};
export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => {
export const toggleAward = (
{ commit, state, getters, dispatch },
{ awardName, noteId },
) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
};
......
......@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const openState = state => state.noteableData.state;
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];
export const notesById = state => state.notes.reduce((acc, note) => {
export const notesById = state =>
state.notes.reduce((acc, note) => {
note.notes.every(n => Object.assign(acc, { [n.id]: n }));
return acc;
}, {});
}, {});
const reverseNotes = array => array.slice(0).reverse();
const isLastNote = (note, state) => !note.system &&
state.userData && note.author &&
const isLastNote = (note, state) =>
!note.system &&
state.userData &&
note.author &&
note.author.id === state.userData.id;
export const getCurrentUserLastNote = state => _.flatten(
reverseNotes(state.notes)
.map(note => reverseNotes(note.notes)),
export const getCurrentUserLastNote = state =>
_.flatten(
reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
).find(el => isLastNote(el, state));
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes)
.find(el => isLastNote(el, state));
export const getDiscussionLastNote = state => discussion =>
reverseNotes(discussion.notes).find(el => isLastNote(el, state));
export const discussionCount = (state) => {
export const discussionCount = state => {
const discussions = state.notes.filter(n => !n.individual_note);
return discussions.length;
......@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
};
export const resolvedDiscussionsById = (state) => {
export const resolvedDiscussionsById = state => {
const map = {};
state.notes.forEach((n) => {
state.notes.forEach(n => {
if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system);
......
......@@ -7,7 +7,7 @@ export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id);
const isDiscussion = (type === constants.DISCUSSION_NOTE);
const isDiscussion = type === constants.DISCUSSION_NOTE;
if (!exists) {
const noteData = {
......@@ -63,13 +63,15 @@ export default {
const note = notes[i];
const children = note.notes;
if (children.length && !note.individual_note) { // remove placeholder from discussions
if (children.length && !note.individual_note) {
// remove placeholder from discussions
for (let j = children.length - 1; j >= 0; j -= 1) {
if (children[j].isPlaceholderNote) {
children.splice(j, 1);
}
}
} else if (note.isPlaceholderNote) { // remove placeholders from state root
} else if (note.isPlaceholderNote) {
// remove placeholders from state root
notes.splice(i, 1);
}
}
......@@ -89,10 +91,10 @@ export default {
[types.SET_INITIAL_NOTES](state, notesData) {
const notes = [];
notesData.forEach((note) => {
notesData.forEach(note => {
// To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => {
note.notes.forEach(n => {
notes.push({
...note,
notes: [n], // override notes array to only have one item to mimick individual_note
......@@ -103,7 +105,7 @@ export default {
notes.push({
...note,
expanded: (oldNote ? oldNote.expanded : note.expanded),
expanded: oldNote ? oldNote.expanded : note.expanded,
});
}
});
......@@ -128,7 +130,9 @@ export default {
notesArr.push({
individual_note: true,
isPlaceholderNote: true,
placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE,
placeholderType: data.isSystemNote
? constants.SYSTEM_NOTE
: constants.NOTE,
notes: [
{
body: data.noteBody,
......@@ -141,12 +145,16 @@ export default {
const { awardName, note } = data;
const { id, name, username } = state.userData;
const hasEmojiAwardedByCurrentUser = note.award_emoji
.filter(emoji => emoji.name === data.awardName && emoji.user.id === id);
const hasEmojiAwardedByCurrentUser = note.award_emoji.filter(
emoji => emoji.name === data.awardName && emoji.user.id === id,
);
if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it.
note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1);
note.award_emoji.splice(
note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
1,
);
} else {
note.award_emoji.push({
name: awardName,
......
......@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
export const findNoteObjectById = (notes, id) =>
notes.filter(n => n.id === id)[0];
export const getQuickActionText = (note) => {
export const getQuickActionText = note => {
let text = 'Applying command';
const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const quickActions =
AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
const executedCommands = quickActions.filter((command) => {
const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(note);
});
......@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
export const stripQuickActions = note =>
note.replace(REGEX_QUICK_ACTIONS, '').trim();
import NotificationsForm from '../../../../notifications_form';
import notificationsDropdown from '../../../../notifications_dropdown';
document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
});
......@@ -16,7 +16,7 @@ ul.notes {
.note-created-ago,
.note-updated-at {
white-space: nowrap;
white-space: normal;
}
.discussion-body {
......
......@@ -16,8 +16,7 @@ class Admin::ProjectsFinder
items = by_archived(items)
items = by_personal(items)
items = by_name(items)
items = sort(items)
items.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
sort(items).page(params[:page])
end
private
......
......@@ -1533,8 +1533,8 @@ class Project < ActiveRecord::Base
@errors = original_errors
end
def add_export_job(current_user:)
job_id = ProjectExportWorker.perform_async(current_user.id, self.id)
def add_export_job(current_user:, params: {})
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
......
......@@ -26,7 +26,7 @@ module Projects
end
def project_tree_saver
Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared)
Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared, params: @params)
end
def uploads_saver
......
......@@ -4,10 +4,11 @@ class ProjectExportWorker
sidekiq_options retry: 3
def perform(current_user_id, project_id)
def perform(current_user_id, project_id, params = {})
params = params.with_indifferent_access
current_user = User.find(current_user_id)
project = Project.find(project_id)
::Projects::ImportExport::ExportService.new(project, current_user).execute
::Projects::ImportExport::ExportService.new(project, current_user, params).execute
end
end
---
title: Adds the option to the project export API to override the project description and display GitLab export description once imported
merge_request: 17744
author:
type: added
---
title: Fix timeouts loading /admin/projects page
merge_request:
author:
type: performance
......@@ -15,9 +15,10 @@ POST /projects/:id/export
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `description` | string | no | Overrides the project description |
```console
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "description=Foo Bar" https://gitlab.example.com/api/v4/projects/1/export
```
```json
......
......@@ -464,7 +464,9 @@ bother us. In any case, it is something to keep in mind when deploying GitLab
on a production cluster.
In order to deploy GitLab on a production cluster, you will need to assign the
GitLab service account to the `anyuid` Security Context.
GitLab service account to the `anyuid` [Security Context Constraints][scc].
For OpenShift v3.0, you will need to do this manually:
1. Edit the Security Context:
```sh
......@@ -477,6 +479,12 @@ GitLab service account to the `anyuid` Security Context.
1. Save and exit the editor
For OpenShift v3.1 and above, you can do:
```sh
oc adm policy add-scc-to-user anyuid system:serviceaccount:gitlab:gitlab-ce-user
```
## Conclusion
By now, you should have an understanding of the basic OpenShift Origin concepts
......@@ -513,3 +521,4 @@ PaaS and managing your applications with the ease of containers.
[autoscaling]: https://docs.openshift.org/latest/dev_guide/pod_autoscaling.html "Documentation - Autoscale"
[basic-cli]: https://docs.openshift.org/latest/cli_reference/basic_cli_operations.html "Documentation - Basic CLI operations"
[openshift-docs]: https://docs.openshift.org "OpenShift documentation"
[scc]: https://docs.openshift.org/latest/admin_guide/manage_scc.html "Documentation - Managing Security Context Constraints"
\ No newline at end of file
......@@ -53,6 +53,22 @@ To get started with the command line, please read through the
Use GitLab's [file finder](../../../workflow/file_finder.md) to search for files in a repository.
### Jupyter Notebook files
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/2508) in GitLab 9.1
[Jupyter][jupyter] Notebook (previously IPython Notebook) files are used for
interactive computing in many fields and contain a complete record of the
user's sessions and include code, narrative text, equations and rich output.
When added to a repository, Jupyter Notebooks with a `.ipynb` extension will be
rendered to HTML when viewed.
![Jupyter Notebook Rich Output](img/jupyter_notebook.png)
Interactive features, including JavaScript plots, will not work when viewed in
GitLab.
## Branches
When you submit changes in a new [branch](branches/index.md), you create a new version
......@@ -158,3 +174,5 @@ Lock your files to prevent any conflicting changes.
## Repository's API
You can access your repos via [repository API](../../../api/repositories.md).
[jupyter]: https://jupyter.org
......@@ -31,8 +31,13 @@ module API
desc 'Start export' do
detail 'This feature was introduced in GitLab 10.6.'
end
params do
optional :description, type: String, desc: 'Override the project description'
end
post ':id/export' do
user_project.add_export_job(current_user: current_user)
project_export_params = declared_params(include_missing: false)
user_project.add_export_job(current_user: current_user, params: project_export_params)
accepted!
end
......
......@@ -52,6 +52,8 @@ module ContainerRegistry
conn.request(:authorization, :bearer, options[:token].to_s)
end
yield(conn) if block_given?
conn.adapter :net_http
end
......@@ -80,8 +82,7 @@ module ContainerRegistry
def faraday
@faraday ||= Faraday.new(@base_uri) do |conn|
initialize_connection(conn, @options)
accept_manifest(conn)
initialize_connection(conn, @options, &method(:accept_manifest))
end
end
......
......@@ -35,6 +35,8 @@ module Gitlab
end
def restored_project
return @project unless @tree_hash
@restored_project ||= restore_project
end
......@@ -81,9 +83,13 @@ module Gitlab
end
def restore_project
return @project unless @tree_hash
params = project_params
if params[:description].present?
params[:description_html] = nil
end
@project.update_columns(project_params)
@project.update_columns(params)
@project
end
......
......@@ -5,7 +5,8 @@ module Gitlab
attr_reader :full_path
def initialize(project:, current_user:, shared:)
def initialize(project:, current_user:, shared:, params: {})
@params = params
@project = project
@current_user = current_user
@shared = shared
......@@ -25,6 +26,10 @@ module Gitlab
private
def project_json_tree
if @params[:description].present?
project_json['description'] = @params[:description]
end
project_json['project_members'] += group_members_json
project_json.to_json
......
......@@ -16,6 +16,6 @@ feature 'User visits the notifications tab', :js do
first('#notifications-button').click
click_link('On mention')
expect(page).to have_content('On mention')
expect(page).to have_selector('#notifications-button', text: 'On mention')
end
end
......@@ -41,6 +41,7 @@ feature 'Import/Export - project import integration test', :js do
project = Project.last
expect(project).not_to be_nil
expect(project.description).to eq("Foo Bar")
expect(project.issues).not_to be_empty
expect(project.merge_requests).not_to be_empty
expect(project_hook_exists?(project)).to be true
......
......@@ -42,6 +42,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED)
end
it 'has the project description' do
expect(Project.find_by_path('project').description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.')
end
it 'has the project html description' do
expect(Project.find_by_path('project').description_html).to eq('description')
end
......
......@@ -29,8 +29,17 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
project_json(project_tree_saver.full_path)
end
context 'with description override' do
let(:params) { { description: 'Foo Bar' } }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared, params: params) }
it 'overrides the project description' do
expect(saved_project_json).to include({ 'description' => params[:description] })
end
end
it 'saves the correct json' do
expect(saved_project_json).to include({ "visibility_level" => 20 })
expect(saved_project_json).to include({ 'description' => 'description', 'visibility_level' => 20 })
end
it 'has approvals_before_merge set' do
......@@ -263,6 +272,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
:issues_disabled,
:wiki_enabled,
:builds_private,
description: 'description',
issues: [issue],
snippets: [snippet],
releases: [release],
......
......@@ -285,6 +285,17 @@ describe API::ProjectExport do
context 'when user is not a member' do
it_behaves_like 'post project export start not found'
end
context 'when overriding description' do
it 'starts' do
params = { description: "Foo" }
expect_any_instance_of(Projects::ImportExport::ExportService).to receive(:execute)
post api(path, project.owner), params
expect(response).to have_gitlab_http_status(202)
end
end
end
end
end
......@@ -285,8 +285,8 @@ production:
export CI_APPLICATION_TAG=$CI_COMMIT_SHA
export CI_CONTAINER_NAME=ci_job_build_${CI_JOB_ID}
export TILLER_NAMESPACE=$KUBE_NAMESPACE
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Static Code Analysis
export SCA_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
function sast_container() {
if [[ -n "$CI_REGISTRY_USER" ]]; then
......@@ -307,11 +307,10 @@ production:
}
function codeclimate() {
docker run --env CODECLIMATE_CODE="$PWD" \
docker run --env SOURCE_CODE="$PWD" \
--volume "$PWD":/code \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume /tmp/cc:/tmp/cc \
"registry.gitlab.com/gitlab-org/security-products/codequality/codeclimate:${SCA_VERSION}" analyze -f json > codeclimate.json
"registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
}
function sast() {
......@@ -328,7 +327,7 @@ production:
--env SAST_DISABLE_REMOTE_CHECKS="${SAST_DISABLE_REMOTE_CHECKS:-false}" \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
"registry.gitlab.com/gitlab-org/security-products/sast:$SCA_VERSION" /app/bin/run /code
"registry.gitlab.com/gitlab-org/security-products/sast:$SP_VERSION" /app/bin/run /code
;;
*)
echo "GitLab EE is required"
......
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