Commit 597eeedb authored by Micaël Bergeron's avatar Micaël Bergeron

Merge remote-tracking branch 'origin/master' into ee-40781-os-to-ce

parents 62365211 e206ec0c
...@@ -315,7 +315,7 @@ stages: ...@@ -315,7 +315,7 @@ stages:
## ##
# Trigger a package build in omnibus-gitlab repository # Trigger a package build in omnibus-gitlab repository
# #
package-qa: package-and-qa:
<<: *dedicated-runner <<: *dedicated-runner
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: [] before_script: []
......
...@@ -109,7 +109,7 @@ gem 'dropzonejs-rails', '~> 0.7.1' ...@@ -109,7 +109,7 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 2.0' gem 'fog-aws', '~> 2.0'
gem 'fog-core', '~> 1.44' gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 1.3.2' gem 'fog-google', '~> 1.3.3'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1' gem 'fog-rackspace', '~> 0.1.1'
...@@ -218,7 +218,7 @@ gem 'asana', '~> 0.6.0' ...@@ -218,7 +218,7 @@ gem 'asana', '~> 0.6.0'
gem 'ruby-fogbugz', '~> 0.2.1' gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration # Kubernetes integration
gem 'kubeclient', '~> 2.2.0' gem 'kubeclient', '~> 3.0'
# d3 # d3
gem 'd3_rails', '~> 3.5.0' gem 'd3_rails', '~> 3.5.0'
...@@ -371,7 +371,7 @@ group :development, :test do ...@@ -371,7 +371,7 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'license_finder', '~> 3.1', require: false gem 'license_finder', '~> 3.1', require: false
gem 'knapsack', '~> 1.11.0' gem 'knapsack', '~> 1.16'
gem 'activerecord_sane_schema_dumper', '0.2' gem 'activerecord_sane_schema_dumper', '0.2'
...@@ -435,9 +435,9 @@ gem 'google-protobuf', '= 3.5.1' ...@@ -435,9 +435,9 @@ gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 1.0.0', require: false gem 'toml-rb', '~> 1.0.0', require: false
# Feature toggles # Feature toggles
gem 'flipper', '~> 0.11.0' gem 'flipper', '~> 0.13.0'
gem 'flipper-active_record', '~> 0.11.0' gem 'flipper-active_record', '~> 0.13.0'
gem 'flipper-active_support_cache_store', '~> 0.11.0' gem 'flipper-active_support_cache_store', '~> 0.13.0'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
......
...@@ -182,7 +182,7 @@ GEM ...@@ -182,7 +182,7 @@ GEM
diff-lcs (1.3) diff-lcs (1.3)
diffy (3.1.0) diffy (3.1.0)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20161021) domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.6) doorkeeper (4.2.6)
railties (>= 4.2) railties (>= 4.2)
...@@ -242,13 +242,13 @@ GEM ...@@ -242,13 +242,13 @@ GEM
path_expander (~> 1.0) path_expander (~> 1.0)
ruby_parser (~> 3.0) ruby_parser (~> 3.0)
sexp_processor (~> 4.0) sexp_processor (~> 4.0)
flipper (0.11.0) flipper (0.13.0)
flipper-active_record (0.11.0) flipper-active_record (0.13.0)
activerecord (>= 3.2, < 6) activerecord (>= 3.2, < 6)
flipper (~> 0.11.0) flipper (~> 0.13.0)
flipper-active_support_cache_store (0.11.0) flipper-active_support_cache_store (0.13.0)
activesupport (>= 3.2, < 6) activesupport (>= 3.2, < 6)
flipper (~> 0.11.0) flipper (~> 0.13.0)
flowdock (0.7.1) flowdock (0.7.1)
httparty (~> 0.7) httparty (~> 0.7)
multi_json multi_json
...@@ -266,7 +266,7 @@ GEM ...@@ -266,7 +266,7 @@ GEM
builder builder
excon (~> 0.58) excon (~> 0.58)
formatador (~> 0.2) formatador (~> 0.2)
fog-google (1.3.2) fog-google (1.3.3)
fog-core fog-core
fog-json fog-json
fog-xml fog-xml
...@@ -430,14 +430,14 @@ GEM ...@@ -430,14 +430,14 @@ GEM
html2text (0.2.0) html2text (0.2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
htmlentities (4.3.4) htmlentities (4.3.4)
http (0.9.8) http (2.2.2)
addressable (~> 2.3) addressable (~> 2.3)
http-cookie (~> 1.0) http-cookie (~> 1.0)
http-form_data (~> 1.0.1) http-form_data (~> 1.0.1)
http_parser.rb (~> 0.6.0) http_parser.rb (~> 0.6.0)
http-cookie (1.0.3) http-cookie (1.0.3)
domain_name (~> 0.5) domain_name (~> 0.5)
http-form_data (1.0.1) http-form_data (1.0.3)
http_parser.rb (0.6.0) http_parser.rb (0.6.0)
httparty (0.13.7) httparty (0.13.7)
json (~> 1.8) json (~> 1.8)
...@@ -479,13 +479,13 @@ GEM ...@@ -479,13 +479,13 @@ GEM
kaminari-core (= 1.0.1) kaminari-core (= 1.0.1)
kaminari-core (1.0.1) kaminari-core (1.0.1)
kgio (2.10.0) kgio (2.10.0)
knapsack (1.11.0) knapsack (1.16.0)
rake rake
timecop (>= 0.1.0) timecop (>= 0.1.0)
kubeclient (2.2.0) kubeclient (3.0.0)
http (= 0.9.8) http (~> 2.2.2)
recursive-open-struct (= 1.0.0) recursive-open-struct (~> 1.0.4)
rest-client rest-client (~> 2.0)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
letter_opener (1.4.1) letter_opener (1.4.1)
...@@ -739,7 +739,7 @@ GEM ...@@ -739,7 +739,7 @@ GEM
re2 (1.1.1) re2 (1.1.1)
recaptcha (3.0.0) recaptcha (3.0.0)
json json
recursive-open-struct (1.0.0) recursive-open-struct (1.0.5)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.3.5) redis (3.3.5)
redis-actionpack (5.0.2) redis-actionpack (5.0.2)
...@@ -767,7 +767,7 @@ GEM ...@@ -767,7 +767,7 @@ GEM
request_store (1.3.1) request_store (1.3.1)
responders (2.3.0) responders (2.3.0)
railties (>= 4.2.0, < 5.1) railties (>= 4.2.0, < 5.1)
rest-client (2.0.0) rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) netrc (~> 0.8)
...@@ -968,7 +968,7 @@ GEM ...@@ -968,7 +968,7 @@ GEM
json (>= 1.8.0) json (>= 1.8.0)
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.4) unf_ext (0.0.7.5)
unicode-display_width (1.3.0) unicode-display_width (1.3.0)
unicorn (5.1.0) unicorn (5.1.0)
kgio (~> 2.6) kgio (~> 2.6)
...@@ -1075,13 +1075,13 @@ DEPENDENCIES ...@@ -1075,13 +1075,13 @@ DEPENDENCIES
fast_blank fast_blank
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.10.0) flay (~> 2.10.0)
flipper (~> 0.11.0) flipper (~> 0.13.0)
flipper-active_record (~> 0.11.0) flipper-active_record (~> 0.13.0)
flipper-active_support_cache_store (~> 0.11.0) flipper-active_support_cache_store (~> 0.13.0)
fog-aliyun (~> 0.2.0) fog-aliyun (~> 0.2.0)
fog-aws (~> 2.0) fog-aws (~> 2.0)
fog-core (~> 1.44) fog-core (~> 1.44)
fog-google (~> 1.3.2) fog-google (~> 1.3.3)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1) fog-rackspace (~> 0.1.1)
...@@ -1126,8 +1126,8 @@ DEPENDENCIES ...@@ -1126,8 +1126,8 @@ DEPENDENCIES
json-schema (~> 2.8.0) json-schema (~> 2.8.0)
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.11.0) knapsack (~> 1.16)
kubeclient (~> 2.2.0) kubeclient (~> 3.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder (~> 3.1) license_finder (~> 3.1)
licensee (~> 8.7.0) licensee (~> 8.7.0)
......
...@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue'; ...@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores'; import store from '../notes/stores';
export default function initMrNotes() { export default function initMrNotes() {
new Vue({ // eslint-disable-line // eslint-disable-next-line no-new
new Vue({
el: '#js-vue-mr-discussions', el: '#js-vue-mr-discussions',
components: { components: {
notesApp, notesApp,
}, },
data() { data() {
const notesDataset = document.getElementById('js-vue-mr-discussions').dataset; const notesDataset = document.getElementById('js-vue-mr-discussions')
.dataset;
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
...@@ -28,7 +30,8 @@ export default function initMrNotes() { ...@@ -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', el: '#js-vue-discussion-counter',
components: { components: {
discussionCounter, discussionCounter,
......
This diff is collapsed.
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import Autosize from 'autosize'; import Autosize from 'autosize';
import { __, sprintf } from '~/locale'; import { __, sprintf } 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 { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility'; import {
import * as constants from '../constants'; capitalizeFirstCharacter,
import eventHub from '../event_hub'; convertToCamelCase,
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; } from '../../lib/utils/text_utility';
import markdownField from '../../vue_shared/components/markdown/field.vue'; import * as constants from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import eventHub from '../event_hub';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import discussionLockedWidget from './discussion_locked_widget.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import issuableStateMixin from '../mixins/issuable_state'; 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', name: 'CommentForm',
components: { components: {
issueWarning, issueWarning,
...@@ -28,9 +31,7 @@ ...@@ -28,9 +31,7 @@
userAvatarLink, userAvatarLink,
loadingButton, loadingButton,
}, },
mixins: [ mixins: [issuableStateMixin],
issuableStateMixin,
],
props: { props: {
noteableType: { noteableType: {
type: String, type: String,
...@@ -53,9 +54,7 @@ ...@@ -53,9 +54,7 @@
'getNotesData', 'getNotesData',
'openState', 'openState',
]), ]),
...mapState([ ...mapState(['isToggleStateButtonLoading']),
'isToggleStateButtonLoading',
]),
noteableDisplayName() { noteableDisplayName() {
return this.noteableType.replace(/_/g, ' '); return this.noteableType.replace(/_/g, ' ');
}, },
...@@ -63,10 +62,15 @@ ...@@ -63,10 +62,15 @@
return this.getUserData.id; return this.getUserData.id;
}, },
commentButtonTitle() { commentButtonTitle() {
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion'; return this.noteType === constants.COMMENT
? 'Comment'
: 'Start discussion';
}, },
isOpen() { isOpen() {
return this.openState === constants.OPENED || this.openState === constants.REOPENED; return (
this.openState === constants.OPENED ||
this.openState === constants.REOPENED
);
}, },
canCreateNote() { canCreateNote() {
return this.getNoteableData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
...@@ -75,23 +79,17 @@ ...@@ -75,23 +79,17 @@
const openOrClose = this.isOpen ? 'close' : 'reopen'; const openOrClose = this.isOpen ? 'close' : 'reopen';
if (this.note.length) { if (this.note.length) {
return sprintf( return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), {
__('%{actionText} & %{openOrClose} %{noteable}'),
{
actionText: this.commentButtonTitle, actionText: this.commentButtonTitle,
openOrClose, openOrClose,
noteable: this.noteableDisplayName, noteable: this.noteableDisplayName,
}, });
);
} }
return sprintf( return sprintf(__('%{openOrClose} %{noteable}'), {
__('%{openOrClose} %{noteable}'),
{
openOrClose: capitalizeFirstCharacter(openOrClose), openOrClose: capitalizeFirstCharacter(openOrClose),
noteable: this.noteableDisplayName, noteable: this.noteableDisplayName,
}, });
);
}, },
actionButtonClassNames() { actionButtonClassNames() {
return { return {
...@@ -131,7 +129,9 @@ ...@@ -131,7 +129,9 @@
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.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED); this.toggleIssueLocalState(
isClosed ? constants.CLOSED : constants.REOPENED,
);
}); });
this.initAutoSave(); this.initAutoSave();
...@@ -180,7 +180,7 @@ ...@@ -180,7 +180,7 @@
this.stopPolling(); this.stopPolling();
this.saveNote(noteData) this.saveNote(noteData)
.then((res) => { .then(res => {
this.enableButton(); this.enableButton();
this.restartPolling(); this.restartPolling();
...@@ -205,8 +205,7 @@ ...@@ -205,8 +205,7 @@
.catch(() => { .catch(() => {
this.enableButton(); this.enableButton();
this.discard(false); this.discard(false);
const msg = const msg = `Your comment could not be submitted!
`Your comment could not be submitted!
Please check your network connection and try again.`; Please check your network connection and try again.`;
Flash(msg, 'alert', this.$el); Flash(msg, 'alert', this.$el);
this.note = noteData.data.note.note; // Restore textarea content. this.note = noteData.data.note.note; // Restore textarea content.
...@@ -228,7 +227,9 @@ Please check your network connection and try again.`; ...@@ -228,7 +227,9 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false); this.toggleStateButtonLoading(false);
Flash( Flash(
sprintf( 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 }, { issuable: this.noteableDisplayName },
), ),
); );
...@@ -241,7 +242,9 @@ Please check your network connection and try again.`; ...@@ -241,7 +242,9 @@ Please check your network connection and try again.`;
this.toggleStateButtonLoading(false); this.toggleStateButtonLoading(false);
Flash( Flash(
sprintf( 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 }, { issuable: this.noteableDisplayName },
), ),
); );
...@@ -278,12 +281,15 @@ Please check your network connection and try again.`; ...@@ -278,12 +281,15 @@ Please check your network connection and try again.`;
}, },
initAutoSave() { initAutoSave() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType)); const noteableType = capitalizeFirstCharacter(
convertToCamelCase(this.noteableType),
this.autosave = new Autosave(
$(this.$refs.textarea),
['Note', noteableType, this.getNoteableData.id],
); );
this.autosave = new Autosave($(this.$refs.textarea), [
'Note',
noteableType,
this.getNoteableData.id,
]);
} }
}, },
initTaskList() { initTaskList() {
...@@ -299,7 +305,7 @@ Please check your network connection and try again.`; ...@@ -299,7 +305,7 @@ Please check your network connection and try again.`;
}); });
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
export default { export default {
components: { components: {
ClipboardButton, ClipboardButton,
Icon, Icon,
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
return this.diffFile.discussionPath ? 'a' : 'span'; return this.diffFile.discussionPath ? 'a' : 'span';
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight'; import syntaxHighlight from '~/syntax_highlight';
import imageDiffHelper from '~/image_diff/helpers/index'; import imageDiffHelper from '~/image_diff/helpers/index';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
export default { export default {
components: { components: {
DiffFileHeader, DiffFileHeader,
}, },
...@@ -37,7 +37,11 @@ ...@@ -37,7 +37,11 @@
if (this.isImageDiff) { if (this.isImageDiff) {
const canCreateNote = false; const canCreateNote = false;
const renderCommentBadge = true; const renderCommentBadge = true;
imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge); imageDiffHelper.initImageDiff(
this.$refs.fileHolder,
canCreateNote,
renderCommentBadge,
);
} else { } else {
const fileHolder = $(this.$refs.fileHolder); const fileHolder = $(this.$refs.fileHolder);
this.$nextTick(() => { this.$nextTick(() => {
...@@ -50,7 +54,7 @@ ...@@ -50,7 +54,7 @@
return html.outerHTML ? 'tr' : 'template'; return html.outerHTML ? 'tr' : 'template';
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import resolveSvg from 'icons/_icon_resolve_discussion.svg'; import resolveSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedSvg from 'icons/_icon_status_success_solid.svg'; import resolvedSvg from 'icons/_icon_status_success_solid.svg';
import mrIssueSvg from 'icons/_icon_mr_issue.svg'; import mrIssueSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionSvg from 'icons/_next_discussion.svg'; import nextDiscussionSvg from 'icons/_next_discussion.svg';
import { pluralize } from '../../lib/utils/text_utility'; import { pluralize } from '../../lib/utils/text_utility';
import { scrollToElement } from '../../lib/utils/common_utils'; import { scrollToElement } from '../../lib/utils/common_utils';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -49,7 +49,9 @@ ...@@ -49,7 +49,9 @@
}, },
methods: { methods: {
jumpToFirstDiscussion() { jumpToFirstDiscussion() {
const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`); const el = document.querySelector(
`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
);
const activeTab = window.mrTabs.currentAction; const activeTab = window.mrTabs.currentAction;
if (activeTab === 'commits' || activeTab === 'pipelines') { if (activeTab === 'commits' || activeTab === 'pipelines') {
...@@ -61,7 +63,7 @@ ...@@ -61,7 +63,7 @@
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import Issuable from '~/vue_shared/mixins/issuable'; import Issuable from '~/vue_shared/mixins/issuable';
export default { export default {
components: { components: {
Icon, Icon,
}, },
mixins: [ mixins: [Issuable],
Issuable, };
],
};
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg'; import editSvg from 'icons/_icon_pencil.svg';
import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'NoteActions', name: 'NoteActions',
directives: { directives: {
tooltip, tooltip,
...@@ -70,9 +70,7 @@ ...@@ -70,9 +70,7 @@
}, },
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['getUserDataByProp']),
'getUserDataByProp',
]),
shouldShowActionsDropdown() { shouldShowActionsDropdown() {
return this.currentUserId && (this.canEdit || this.canReportAsAbuse); return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
}, },
...@@ -115,7 +113,7 @@ ...@@ -115,7 +113,7 @@
this.$emit('handleResolve'); this.$emit('handleResolve');
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
export default { export default {
name: 'NoteAttachment', name: 'NoteAttachment',
props: { props: {
attachment: { attachment: {
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
required: true, required: true,
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg'; import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
import emojiSmile from 'icons/_emoji_smile.svg'; import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg'; import emojiSmiley from 'icons/_emoji_smiley.svg';
import Flash from '../../flash'; import Flash from '../../flash';
import { glEmojiTag } from '../../emoji'; import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -30,9 +30,7 @@ ...@@ -30,9 +30,7 @@
}, },
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['getUserData']),
'getUserData',
]),
// `this.awards` is an array with emojis but they are not grouped by emoji name. See below. // `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 } ] // [ { 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. // This method will group emojis by their name as an Object. See below.
...@@ -79,9 +77,7 @@ ...@@ -79,9 +77,7 @@
this.emojiSmiley = emojiSmiley; this.emojiSmiley = emojiSmiley;
}, },
methods: { methods: {
...mapActions([ ...mapActions(['toggleAwardRequest']),
'toggleAwardRequest',
]),
getAwardHTML(name) { getAwardHTML(name) {
return glEmojiTag(name); return glEmojiTag(name);
}, },
...@@ -96,30 +92,43 @@ ...@@ -96,30 +92,43 @@
const restrictedEmojis = ['thumbsup', 'thumbsdown']; const restrictedEmojis = ['thumbsup', 'thumbsdown'];
// Users can not add :+1: and :-1: to their own notes // 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; isAllowed = false;
} }
return this.getUserData.id && isAllowed; return this.getUserData.id && isAllowed;
}, },
hasReactionByCurrentUser(awardList) { 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) { awardTitle(awardsList) {
const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList); const hasReactionByCurrentUser = this.hasReactionByCurrentUser(
awardsList,
);
const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10; const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
let awardList = awardsList; let awardList = awardsList;
// Filter myself from list if I am awarded. // Filter myself from list if I am awarded.
if (hasReactionByCurrentUser) { 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. // 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. // 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. // Add myself to the begining of the list so title will start with You.
if (hasReactionByCurrentUser) { if (hasReactionByCurrentUser) {
...@@ -130,14 +139,17 @@ ...@@ -130,14 +139,17 @@
// We have 10+ awarded user, join them with comma and add `and x more`. // We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) { if (remainingAwardList.length) {
title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`; title = `${namesToShow.join(', ')}, and ${
remainingAwardList.length
} more.`;
} else if (namesToShow.length > 1) { } else if (namesToShow.length > 1) {
// Join all names with comma but not the last one, it will be added with and text. // 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(', '); title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
// If we have more than 2 users we need an extra comma before and text. // If we have more than 2 users we need an extra comma before and text.
title += namesToShow.length > 2 ? ',' : ''; title += namesToShow.length > 2 ? ',' : '';
title += ` and ${namesToShow.slice(-1)}`; // Append and text 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 '); title = namesToShow.join(' and ');
} }
...@@ -169,11 +181,12 @@ ...@@ -169,11 +181,12 @@
awardName: parsedName, awardName: parsedName,
}; };
this.toggleAwardRequest(data) this.toggleAwardRequest(data).catch(() =>
.catch(() => Flash('Something went wrong on our end.')); Flash('Something went wrong on our end.'),
);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import noteEditedText from './note_edited_text.vue'; import noteEditedText from './note_edited_text.vue';
import noteAwardsList from './note_awards_list.vue'; import noteAwardsList from './note_awards_list.vue';
import noteAttachment from './note_attachment.vue'; import noteAttachment from './note_attachment.vue';
import noteForm from './note_form.vue'; import noteForm from './note_form.vue';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
components: { components: {
noteEditedText, noteEditedText,
noteAwardsList, noteAwardsList,
noteAttachment, noteAttachment,
noteForm, noteForm,
}, },
mixins: [ mixins: [autosave],
autosave,
],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -77,7 +75,7 @@ ...@@ -77,7 +75,7 @@
this.$emit('cancelFormEdition', shouldConfirm, isDirty); this.$emit('cancelFormEdition', shouldConfirm, isDirty);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <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', name: 'EditedNoteText',
components: { components: {
timeAgoTooltip, timeAgoTooltip,
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
default: 'edited-text', default: 'edited-text',
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
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 markdownField from '../../vue_shared/components/markdown/field.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state'; import issuableStateMixin from '../mixins/issuable_state';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
name: 'IssueNoteForm', name: 'IssueNoteForm',
components: { components: {
issueWarning, issueWarning,
markdownField, markdownField,
}, },
mixins: [ mixins: [issuableStateMixin, resolvable],
issuableStateMixin,
resolvable,
],
props: { props: {
noteBody: { noteBody: {
type: String, type: String,
...@@ -69,7 +66,9 @@ ...@@ -69,7 +66,9 @@
return this.getNotesDataByProp('markdownDocsPath'); return this.getNotesDataByProp('markdownDocsPath');
}, },
quickActionsDocsPath() { quickActionsDocsPath() {
return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined; return !this.isEditing
? this.getNotesDataByProp('quickActionsDocsPath')
: undefined;
}, },
currentUserId() { currentUserId() {
return this.getUserDataByProp('id'); return this.getUserDataByProp('id');
...@@ -91,24 +90,29 @@ ...@@ -91,24 +90,29 @@
this.$refs.textarea.focus(); this.$refs.textarea.focus();
}, },
methods: { methods: {
...mapActions([ ...mapActions(['toggleResolveNote']),
'toggleResolveNote',
]),
handleUpdate(shouldResolve) { handleUpdate(shouldResolve) {
const beforeSubmitDiscussionState = this.discussionResolved; const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true; this.isSubmitting = true;
this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { this.$emit(
'handleFormUpdate',
this.updatedNoteBody,
this.$refs.editNoteForm,
() => {
this.isSubmitting = false; this.isSubmitting = false;
if (shouldResolve) { if (shouldResolve) {
this.resolveHandler(beforeSubmitDiscussionState); this.resolveHandler(beforeSubmitDiscussionState);
} }
}); },
);
}, },
editMyLastNote() { editMyLastNote() {
if (this.updatedNoteBody === '') { if (this.updatedNoteBody === '') {
const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody); const lastNoteInDiscussion = this.getDiscussionLastNote(
this.updatedNoteBody,
);
if (lastNoteInDiscussion) { if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', { eventHub.$emit('enterEditMode', {
...@@ -119,10 +123,14 @@ ...@@ -119,10 +123,14 @@
}, },
cancelHandler(shouldConfirm = false) { cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed // 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> </script>
<template> <template>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
export default { export default {
components: { components: {
timeAgoTooltip, timeAgoTooltip,
}, },
...@@ -49,9 +49,7 @@ ...@@ -49,9 +49,7 @@
}, },
}, },
methods: { methods: {
...mapActions([ ...mapActions(['setTargetNoteHash']),
'setTargetNoteHash',
]),
handleToggle() { handleToggle() {
this.$emit('toggleHandler'); this.$emit('toggleHandler');
}, },
...@@ -59,7 +57,7 @@ ...@@ -59,7 +57,7 @@
this.setTargetNoteHash(this.noteTimestampLink); this.setTargetNoteHash(this.noteTimestampLink);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
computed: { computed: {
...mapGetters([ ...mapGetters(['getNotesDataByProp']),
'getNotesDataByProp',
]),
registerLink() { registerLink() {
return this.getNotesDataByProp('registerPath'); return this.getNotesDataByProp('registerPath');
}, },
...@@ -13,7 +11,7 @@ ...@@ -13,7 +11,7 @@
return this.getNotesDataByProp('newSessionPath'); return this.getNotesDataByProp('newSessionPath');
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg'; import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
import nextDiscussionsSvg from 'icons/_next_discussion.svg'; import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import Flash from '../../flash'; import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants'; import { SYSTEM_NOTE } from '../constants';
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 noteableNote from './noteable_note.vue'; import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue'; import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue'; import noteForm from './note_form.vue';
import diffWithNote from './diff_with_note.vue'; import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { scrollToElement } from '../../lib/utils/common_utils'; import { scrollToElement } from '../../lib/utils/common_utils';
export default { export default {
components: { components: {
noteableNote, noteableNote,
diffWithNote, diffWithNote,
...@@ -34,11 +34,7 @@ ...@@ -34,11 +34,7 @@
directives: { directives: {
tooltip, tooltip,
}, },
mixins: [ mixins: [autosave, noteable, resolvable],
autosave,
noteable,
resolvable,
],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -99,7 +95,9 @@ ...@@ -99,7 +95,9 @@
return this.unresolvedDiscussions.length > 0; return this.unresolvedDiscussions.length > 0;
}, },
wrapperComponent() { wrapperComponent() {
return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div'; return this.discussion.diffDiscussion && this.discussion.diffFile
? diffWithNote
: 'div';
}, },
wrapperClass() { wrapperClass() {
return this.isDiffDiscussion ? '' : 'panel panel-default'; return this.isDiffDiscussion ? '' : 'panel panel-default';
...@@ -151,8 +149,10 @@ ...@@ -151,8 +149,10 @@
}, },
cancelReplyForm(shouldConfirm) { cancelReplyForm(shouldConfirm) {
if (shouldConfirm && this.$refs.noteForm.isDirty) { if (shouldConfirm && this.$refs.noteForm.isDirty) {
const msg = 'Are you sure you want to cancel creating this comment?';
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
if (!confirm('Are you sure you want to cancel creating this comment?')) { if (!confirm(msg)) {
return; return;
} }
} }
...@@ -178,7 +178,7 @@ ...@@ -178,7 +178,7 @@
this.resetAutoSave(); this.resetAutoSave();
callback(); callback();
}) })
.catch((err) => { .catch(err => {
this.removePlaceholderNotes(); this.removePlaceholderNotes();
this.isReplying = true; this.isReplying = true;
this.$nextTick(() => { this.$nextTick(() => {
...@@ -204,7 +204,7 @@ Please check your network connection and try again.`; ...@@ -204,7 +204,7 @@ Please check your network connection and try again.`;
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore'; import { escape } from 'underscore';
import Flash from '../../flash'; import Flash from '../../flash';
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 noteHeader from './note_header.vue'; import noteHeader from './note_header.vue';
import noteActions from './note_actions.vue'; import noteActions from './note_actions.vue';
import noteBody from './note_body.vue'; import noteBody from './note_body.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import noteable from '../mixins/noteable'; import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable'; import resolvable from '../mixins/resolvable';
export default { export default {
components: { components: {
userAvatarLink, userAvatarLink,
noteHeader, noteHeader,
noteActions, noteActions,
noteBody, noteBody,
}, },
mixins: [ mixins: [noteable, resolvable],
noteable,
resolvable,
],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -37,10 +34,7 @@ ...@@ -37,10 +34,7 @@
}; };
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['targetNoteHash', 'getUserData']),
'targetNoteHash',
'getUserData',
]),
author() { author() {
return this.note.author; return this.note.author;
}, },
...@@ -53,7 +47,9 @@ ...@@ -53,7 +47,9 @@
}; };
}, },
canReportAsAbuse() { 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() { noteAnchorId() {
return `note_${this.note.id}`; return `note_${this.note.id}`;
...@@ -89,7 +85,9 @@ ...@@ -89,7 +85,9 @@
this.isDeleting = false; this.isDeleting = false;
}) })
.catch(() => { .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; this.isDeleting = false;
}); });
} }
...@@ -120,7 +118,8 @@ ...@@ -120,7 +118,8 @@
this.isRequesting = false; this.isRequesting = false;
this.isEditing = true; this.isEditing = true;
this.$nextTick(() => { 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); Flash(msg, 'alert', this.$el);
this.recoverNoteContent(noteText); this.recoverNoteContent(noteText);
callback(); callback();
...@@ -130,7 +129,8 @@ ...@@ -130,7 +129,8 @@
formCancelHandler(shouldConfirm, isDirty) { formCancelHandler(shouldConfirm, isDirty) {
if (shouldConfirm && isDirty) { if (shouldConfirm && isDirty) {
// eslint-disable-next-line no-alert // 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(); this.$refs.noteBody.resetAutoSave();
if (this.oldContent) { if (this.oldContent) {
...@@ -146,7 +146,7 @@ ...@@ -146,7 +146,7 @@
this.$refs.noteBody.$refs.noteForm.note.note = noteText; this.$refs.noteBody.$refs.noteForm.note.note = noteText;
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility'; import { getLocationHash } from '../../lib/utils/url_utility';
import Flash from '../../flash'; import Flash from '../../flash';
import store from '../stores/'; import store from '../stores/';
import * as constants from '../constants'; import * as constants from '../constants';
import noteableNote from './noteable_note.vue'; import noteableNote from './noteable_note.vue';
import noteableDiscussion from './noteable_discussion.vue'; import noteableDiscussion from './noteable_discussion.vue';
import systemNote from '../../vue_shared/components/notes/system_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue';
import commentForm from './comment_form.vue'; import commentForm from './comment_form.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default { export default {
name: 'NotesApp', name: 'NotesApp',
components: { components: {
noteableNote, noteableNote,
...@@ -47,16 +47,14 @@ ...@@ -47,16 +47,14 @@
}; };
}, },
computed: { computed: {
...mapGetters([ ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
'notes',
'getNotesDataByProp',
'discussionCount',
]),
noteableType() { noteableType() {
// FIXME -- @fatihacet Get this from JSON data. // FIXME -- @fatihacet Get this from JSON data.
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; 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() { allNotes() {
if (this.isLoading) { if (this.isLoading) {
...@@ -79,9 +77,11 @@ ...@@ -79,9 +77,11 @@
const parentElement = this.$el.parentElement; const parentElement = this.$el.parentElement;
if (parentElement && if (
parentElement.classList.contains('js-vue-notes-event')) { parentElement &&
parentElement.addEventListener('toggleAward', (event) => { parentElement.classList.contains('js-vue-notes-event')
) {
parentElement.addEventListener('toggleAward', event => {
const { awardName, noteId } = event.detail; const { awardName, noteId } = event.detail;
this.actionToggleAward({ awardName, noteId }); this.actionToggleAward({ awardName, noteId });
}); });
...@@ -131,7 +131,9 @@ ...@@ -131,7 +131,9 @@
.then(() => this.checkLocationHash()) .then(() => this.checkLocationHash())
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
Flash('Something went wrong while fetching comments. Please try again.'); Flash(
'Something went wrong while fetching comments. Please try again.',
);
}); });
}, },
initPolling() { initPolling() {
...@@ -154,7 +156,7 @@ ...@@ -154,7 +156,7 @@
} }
}, },
}, },
}; };
</script> </script>
<template> <template>
......
import Vue from 'vue'; import Vue from 'vue';
import notesApp from './components/notes_app.vue'; import notesApp from './components/notes_app.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener(
'DOMContentLoaded',
() =>
new Vue({
el: '#js-vue-notes', el: '#js-vue-notes',
components: { components: {
notesApp, notesApp,
...@@ -9,13 +12,17 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -9,13 +12,17 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
data() { data() {
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData); const parsedUserData = JSON.parse(notesDataset.currentUserData);
const currentUserData = parsedUserData ? { let currentUserData = {};
if (parsedUserData) {
currentUserData = {
id: parsedUserData.id, id: parsedUserData.id,
name: parsedUserData.name, name: parsedUserData.name,
username: parsedUserData.username, username: parsedUserData.username,
avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
path: parsedUserData.path, path: parsedUserData.path,
} : {}; };
}
return { return {
noteableData: JSON.parse(notesDataset.noteableData), noteableData: JSON.parse(notesDataset.noteableData),
...@@ -32,4 +39,5 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -32,4 +39,5 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
}, },
}); });
}, },
})); }),
);
...@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility'; ...@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default { export default {
methods: { methods: {
initAutoSave(noteableType) { 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() { resetAutoSave() {
this.autosave.reset(); this.autosave.reset();
......
...@@ -12,7 +12,8 @@ export default { ...@@ -12,7 +12,8 @@ export default {
discussionResolved() { discussionResolved() {
const { notes, resolved } = this.note; 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); return notes.every(note => note.resolved && !note.system);
} }
...@@ -26,7 +27,9 @@ export default { ...@@ -26,7 +27,9 @@ export default {
return __('Comment and resolve discussion'); return __('Comment and resolve discussion');
} }
return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion'); return this.discussionResolved
? __('Unresolve discussion')
: __('Resolve discussion');
}, },
}, },
methods: { methods: {
...@@ -42,7 +45,9 @@ export default { ...@@ -42,7 +45,9 @@ export default {
}) })
.catch(() => { .catch(() => {
this.isResolving = false; 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); Flash(msg, 'alert', this.$el);
}); });
}, },
......
...@@ -22,7 +22,9 @@ export default { ...@@ -22,7 +22,9 @@ export default {
}, },
toggleResolveNote(endpoint, isResolved) { toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants; 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); return Vue.http[method](endpoint);
}, },
......
...@@ -12,47 +12,57 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; ...@@ -12,47 +12,57 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll; let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); export const setNotesData = ({ commit }, data) =>
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data); commit(types.SET_NOTES_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); export const setNoteableData = ({ commit }, data) =>
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); commit(types.SET_NOTEABLE_DATA, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); export const setUserData = ({ commit }, data) =>
export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data); commit(types.SET_USER_DATA, data);
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const setLastFetchedAt = ({ commit }, data) =>
commit(types.SET_LAST_FETCHED_AT, data);
export const fetchNotes = ({ commit }, path) => service 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) .fetchNotes(path)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
commit(types.SET_INITIAL_NOTES, res); commit(types.SET_INITIAL_NOTES, res);
}); });
export const deleteNote = ({ commit }, note) => service export const deleteNote = ({ commit }, note) =>
.deleteNote(note.path) service.deleteNote(note.path).then(() => {
.then(() => {
commit(types.DELETE_NOTE, note); commit(types.DELETE_NOTE, note);
}); });
export const updateNote = ({ commit }, { endpoint, note }) => service export const updateNote = ({ commit }, { endpoint, note }) =>
service
.updateNote(endpoint, note) .updateNote(endpoint, note)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
commit(types.UPDATE_NOTE, res); commit(types.UPDATE_NOTE, res);
}); });
export const replyToDiscussion = ({ commit }, { endpoint, data }) => service export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
service
.replyToDiscussion(endpoint, data) .replyToDiscussion(endpoint, data)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
return res; return res;
}); });
export const createNewNote = ({ commit }, { endpoint, data }) => service export const createNewNote = ({ commit }, { endpoint, data }) =>
service
.createNewNote(endpoint, data) .createNewNote(endpoint, data)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
if (!res.errors) { if (!res.errors) {
commit(types.ADD_NEW_NOTE, res); commit(types.ADD_NEW_NOTE, res);
} }
...@@ -62,11 +72,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service ...@@ -62,11 +72,17 @@ 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 toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service export const toggleResolveNote = (
{ commit },
{ endpoint, isResolved, discussion },
) =>
service
.toggleResolveNote(endpoint, isResolved) .toggleResolveNote(endpoint, isResolved)
.then(res => res.json()) .then(res => res.json())
.then((res) => { .then(res => {
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; const mutationType = discussion
? types.UPDATE_DISCUSSION
: types.UPDATE_NOTE;
commit(mutationType, res); commit(mutationType, res);
}); });
...@@ -76,7 +92,7 @@ export const closeIssue = ({ commit, dispatch, state }) => { ...@@ -76,7 +92,7 @@ export const closeIssue = ({ commit, dispatch, state }) => {
return service return service
.toggleIssueState(state.notesData.closePath) .toggleIssueState(state.notesData.closePath)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then(data => {
commit(types.CLOSE_ISSUE); commit(types.CLOSE_ISSUE);
dispatch('emitStateChangedEvent', data); dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false); dispatch('toggleStateButtonLoading', false);
...@@ -88,7 +104,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => { ...@@ -88,7 +104,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
return service return service
.toggleIssueState(state.notesData.reopenPath) .toggleIssueState(state.notesData.reopenPath)
.then(res => res.json()) .then(res => res.json())
.then((data) => { .then(data => {
commit(types.REOPEN_ISSUE); commit(types.REOPEN_ISSUE);
dispatch('emitStateChangedEvent', data); dispatch('emitStateChangedEvent', data);
dispatch('toggleStateButtonLoading', false); dispatch('toggleStateButtonLoading', false);
...@@ -99,10 +115,12 @@ export const toggleStateButtonLoading = ({ commit }, value) => ...@@ -99,10 +115,12 @@ export const toggleStateButtonLoading = ({ commit }, value) =>
commit(types.TOGGLE_STATE_BUTTON_LOADING, value); commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
export const emitStateChangedEvent = ({ commit, getters }, data) => { 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, data,
isClosed: getters.openState === constants.CLOSED, isClosed: getters.openState === constants.CLOSED,
} }); },
});
document.dispatchEvent(event); document.dispatchEvent(event);
}; };
...@@ -144,8 +162,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -144,8 +162,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}); });
} }
return dispatch(methodToDispatch, noteData) return dispatch(methodToDispatch, noteData).then(res => {
.then((res) => {
const { errors } = res; const { errors } = res;
const commandsChanges = res.commands_changes; const commandsChanges = res.commands_changes;
...@@ -161,8 +178,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -161,8 +178,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const votesBlock = $('.js-awards-block').eq(0); const votesBlock = $('.js-awards-block').eq(0);
loadAwardsHandler() loadAwardsHandler()
.then((awardsHandler) => { .then(awardsHandler => {
awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award); awardsHandler.addAwardToEmojiBar(
votesBlock,
commandsChanges.emoji_award,
);
awardsHandler.scrollToAwards(); awardsHandler.scrollToAwards();
}) })
.catch(() => { .catch(() => {
...@@ -174,7 +194,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -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); sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
} }
} }
...@@ -192,11 +215,17 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { ...@@ -192,11 +215,17 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
if (resp.notes && resp.notes.length) { if (resp.notes && resp.notes.length) {
const { notesById } = getters; const { notesById } = getters;
resp.notes.forEach((note) => { resp.notes.forEach(note => {
if (notesById[note.id]) { if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note); commit(types.UPDATE_NOTE, note);
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) { } else if (
const discussion = utils.findNoteObjectById(state.notes, note.discussion_id); note.type === constants.DISCUSSION_NOTE ||
note.type === constants.DIFF_NOTE
) {
const discussion = utils.findNoteObjectById(
state.notes,
note.discussion_id,
);
if (discussion) { if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
...@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => { ...@@ -219,9 +248,12 @@ export const poll = ({ commit, state, getters }) => {
resource: service, resource: service,
method: 'poll', method: 'poll',
data: state, data: state,
successCallback: resp => resp.json() successCallback: resp =>
resp
.json()
.then(data => pollSuccessCallBack(data, commit, state, getters)), .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()) { if (!Visibility.hidden()) {
...@@ -248,15 +280,22 @@ export const restartPolling = () => { ...@@ -248,15 +280,22 @@ export const restartPolling = () => {
}; };
export const fetchData = ({ commit, state, getters }) => { 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(resp => resp.json)
.then(data => pollSuccessCallBack(data, commit, state, getters)) .then(data => pollSuccessCallBack(data, commit, state, getters))
.catch(() => Flash('Something went wrong while fetching latest comments.')); .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] }); commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
}; };
......
...@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop]; ...@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const openState = state => state.noteableData.state; export const openState = 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];
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 })); note.notes.every(n => Object.assign(acc, { [n.id]: n }));
return acc; return acc;
}, {}); }, {});
const reverseNotes = array => array.slice(0).reverse(); const reverseNotes = array => array.slice(0).reverse();
const isLastNote = (note, state) => !note.system && const isLastNote = (note, state) =>
state.userData && note.author && !note.system &&
state.userData &&
note.author &&
note.author.id === state.userData.id; note.author.id === state.userData.id;
export const getCurrentUserLastNote = state => _.flatten( export const getCurrentUserLastNote = state =>
reverseNotes(state.notes) _.flatten(
.map(note => reverseNotes(note.notes)), reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
).find(el => isLastNote(el, state)); ).find(el => isLastNote(el, state));
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes) export const getDiscussionLastNote = state => discussion =>
.find(el => isLastNote(el, state)); 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); const discussions = state.notes.filter(n => !n.individual_note);
return discussions.length; return discussions.length;
...@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => { ...@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]); return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
}; };
export const resolvedDiscussionsById = (state) => { export const resolvedDiscussionsById = state => {
const map = {}; const map = {};
state.notes.forEach((n) => { state.notes.forEach(n => {
if (n.notes) { if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system); const resolved = n.notes.every(note => note.resolved && !note.system);
......
...@@ -7,7 +7,7 @@ export default { ...@@ -7,7 +7,7 @@ export default {
[types.ADD_NEW_NOTE](state, note) { [types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note; const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id); 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) { if (!exists) {
const noteData = { const noteData = {
...@@ -63,13 +63,15 @@ export default { ...@@ -63,13 +63,15 @@ export default {
const note = notes[i]; const note = notes[i];
const children = note.notes; 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) { for (let j = children.length - 1; j >= 0; j -= 1) {
if (children[j].isPlaceholderNote) { if (children[j].isPlaceholderNote) {
children.splice(j, 1); 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); notes.splice(i, 1);
} }
} }
...@@ -89,10 +91,10 @@ export default { ...@@ -89,10 +91,10 @@ export default {
[types.SET_INITIAL_NOTES](state, notesData) { [types.SET_INITIAL_NOTES](state, notesData) {
const notes = []; const notes = [];
notesData.forEach((note) => { notesData.forEach(note => {
// To support legacy notes, should be very rare case. // To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) { if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => { note.notes.forEach(n => {
notes.push({ notes.push({
...note, ...note,
notes: [n], // override notes array to only have one item to mimick individual_note notes: [n], // override notes array to only have one item to mimick individual_note
...@@ -103,7 +105,7 @@ export default { ...@@ -103,7 +105,7 @@ export default {
notes.push({ notes.push({
...note, ...note,
expanded: (oldNote ? oldNote.expanded : note.expanded), expanded: oldNote ? oldNote.expanded : note.expanded,
}); });
} }
}); });
...@@ -128,7 +130,9 @@ export default { ...@@ -128,7 +130,9 @@ export default {
notesArr.push({ notesArr.push({
individual_note: true, individual_note: true,
isPlaceholderNote: true, isPlaceholderNote: true,
placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE, placeholderType: data.isSystemNote
? constants.SYSTEM_NOTE
: constants.NOTE,
notes: [ notes: [
{ {
body: data.noteBody, body: data.noteBody,
...@@ -141,12 +145,16 @@ export default { ...@@ -141,12 +145,16 @@ export default {
const { awardName, note } = data; const { awardName, note } = data;
const { id, name, username } = state.userData; const { id, name, username } = state.userData;
const hasEmojiAwardedByCurrentUser = note.award_emoji const hasEmojiAwardedByCurrentUser = note.award_emoji.filter(
.filter(emoji => emoji.name === data.awardName && emoji.user.id === id); emoji => emoji.name === data.awardName && emoji.user.id === id,
);
if (hasEmojiAwardedByCurrentUser.length) { if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it. // 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 { } else {
note.award_emoji.push({ note.award_emoji.push({
name: awardName, name: awardName,
......
...@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache'; ...@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; 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'; 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}`); const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(note); return commandRegex.test(note);
}); });
...@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => { ...@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(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();
});
import $ from 'jquery'; import $ from 'jquery';
import initBlob from '~/blob_edit/blob_bundle';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
import NotificationsForm from '~/notifications_form'; import NotificationsForm from '~/notifications_form';
import UserCallout from '~/user_callout'; import UserCallout from '~/user_callout';
...@@ -19,10 +20,22 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -19,10 +20,22 @@ document.addEventListener('DOMContentLoaded', () => {
className: 'js-autodevops-banner', className: 'js-autodevops-banner',
}); });
if ($('#tree-slider').length) new TreeView(); // eslint-disable-line no-new // Project show page loads different overview content based on user preferences
if ($('.blob-viewer').length) new BlobViewer(); // eslint-disable-line no-new const treeSlider = document.querySelector('#tree-slider');
if ($('.project-show-activity').length) new Activities(); // eslint-disable-line no-new if (treeSlider) {
$('#tree-slider').waitForImages(() => { new TreeView(); // eslint-disable-line no-new
initBlob();
}
if (document.querySelector('.blob-viewer')) {
new BlobViewer(); // eslint-disable-line no-new
}
if (document.querySelector('.project-show-activity')) {
new Activities(); // eslint-disable-line no-new
}
$(treeSlider).waitForImages(() => {
ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
}); });
}); });
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetUnresolvedDiscussions',
props: {
mr: { type: Object, required: true },
},
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
</span>
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
class="btn btn-default btn-xs js-create-issue">
Create an issue to resolve them later
</a>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'UnresolvedDiscussions',
components: {
statusIcon,
},
props: {
mr: { type: Object, required: true },
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
There are unresolved discussions. Please resolve these discussions
</span>
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
class="btn btn-default btn-xs js-create-issue">
Create an issue to resolve them later
</a>
</div>
</div>
</template>
...@@ -29,7 +29,7 @@ export { default as MissingBranchState } from './components/states/mr_widget_mis ...@@ -29,7 +29,7 @@ export { default as MissingBranchState } from './components/states/mr_widget_mis
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue'; export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge'; export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch'; export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions'; export { default as UnresolvedDiscussionsState } from './components/states/unresolved_discussions.vue';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue'; export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed'; export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
......
...@@ -16,7 +16,7 @@ ul.notes { ...@@ -16,7 +16,7 @@ ul.notes {
.note-created-ago, .note-created-ago,
.note-updated-at { .note-updated-at {
white-space: nowrap; white-space: normal;
} }
.discussion-body { .discussion-body {
......
...@@ -132,13 +132,35 @@ ...@@ -132,13 +132,35 @@
.multi-file-tabs { .multi-file-tabs {
display: flex; display: flex;
overflow-x: auto;
background-color: $white-normal; background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark; box-shadow: inset 0 -1px $white-dark;
> li { > ul {
display: flex;
overflow-x: auto;
}
li {
position: relative; position: relative;
} }
.dropdown {
display: flex;
margin-left: auto;
margin-bottom: 1px;
padding: 0 $grid-size;
border-left: 1px solid $white-dark;
background-color: $white-light;
&.shadow {
box-shadow: 0 0 10px $dropdown-shadow-color;
}
.btn {
margin-top: auto;
margin-bottom: auto;
}
}
} }
.multi-file-tab { .multi-file-tab {
...@@ -207,6 +229,70 @@ ...@@ -207,6 +229,70 @@
.vertical-center { .vertical-center {
min-height: auto; min-height: auto;
} }
.monaco-editor .lines-content .cigr {
display: none;
}
.monaco-diff-editor.vs {
.editor.modified {
box-shadow: none;
}
.diagonal-fill {
display: none !important;
}
.diffOverview {
background-color: $white-light;
border-left: 1px solid $white-dark;
cursor: ns-resize;
}
.diffViewport {
display: none;
}
.char-insert {
background-color: $line-added-dark;
}
.char-delete {
background-color: $line-removed-dark;
}
.line-numbers {
color: $black-transparent;
}
.view-overlays {
.line-insert {
background-color: $line-added;
}
.line-delete {
background-color: $line-removed;
}
}
.margin {
background-color: $gray-light;
border-right: 1px solid $white-normal;
.line-insert {
border-right: 1px solid $line-added-dark;
}
.line-delete {
border-right: 1px solid $line-removed-dark;
}
}
.margin-view-overlays .insert-sign,
.margin-view-overlays .delete-sign {
opacity: .4;
}
}
} }
.multi-file-editor-holder { .multi-file-editor-holder {
...@@ -596,11 +682,6 @@ ...@@ -596,11 +682,6 @@
padding-bottom: 0; padding-bottom: 0;
} }
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $context-header-height});
min-height: calc(100vh - #{$header-height + $context-header-height});
}
&.flash-shown { &.flash-shown {
.content-wrapper { .content-wrapper {
margin-top: 0; margin-top: 0;
...@@ -609,10 +690,11 @@ ...@@ -609,10 +690,11 @@
.ide-view { .ide-view {
height: calc(100vh - #{$header-height + $flash-height}); height: calc(100vh - #{$header-height + $flash-height});
} }
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll { .projects-sidebar {
max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height}); .multi-file-commit-panel-inner-scroll {
min-height: calc(100vh - #{$header-height + $flash-height + $context-header-height}); flex: 1;
} }
} }
} }
...@@ -632,11 +714,6 @@ ...@@ -632,11 +714,6 @@
height: calc(100vh - #{$header-height + $performance-bar-height}); height: calc(100vh - #{$header-height + $performance-bar-height});
} }
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
min-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
}
&.flash-shown { &.flash-shown {
.content-wrapper { .content-wrapper {
margin-top: 0; margin-top: 0;
...@@ -645,11 +722,6 @@ ...@@ -645,11 +722,6 @@
.ide-view { .ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height}); height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
} }
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
min-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
}
} }
} }
...@@ -684,3 +756,29 @@ ...@@ -684,3 +756,29 @@
.ide-commit-new-branch { .ide-commit-new-branch {
margin-left: 25px; margin-left: 25px;
} }
.ide-external-links {
p {
margin: 0;
}
}
.ide-sidebar-link {
padding: $gl-padding-8 $gl-padding;
background: $indigo-700;
color: $white-light;
text-decoration: none;
display: flex;
align-items: center;
&:focus,
&:hover {
color: $white-light;
text-decoration: underline;
background: $indigo-500;
}
&:active {
background: $indigo-800;
}
}
...@@ -16,8 +16,7 @@ class Admin::ProjectsFinder ...@@ -16,8 +16,7 @@ class Admin::ProjectsFinder
items = by_archived(items) items = by_archived(items)
items = by_personal(items) items = by_personal(items)
items = by_name(items) items = by_name(items)
items = sort(items) sort(items).page(params[:page])
items.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
end end
private private
......
...@@ -348,15 +348,15 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -348,15 +348,15 @@ class ApplicationSetting < ActiveRecord::Base
end end
def home_page_url_column_exists? def home_page_url_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url) ::Gitlab::Database.cached_column_exists?(:application_settings, :home_page_url)
end end
def help_page_support_url_column_exists? def help_page_support_url_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :help_page_support_url) ::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url)
end end
def sidekiq_throttling_column_exists? def sidekiq_throttling_column_exists?
ActiveRecord::Base.connection.column_exists?(:application_settings, :sidekiq_throttling_enabled) ::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled)
end end
def domain_whitelist_raw def domain_whitelist_raw
......
...@@ -45,15 +45,13 @@ module Ci ...@@ -45,15 +45,13 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) } scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) } scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts_archive, ->() do
scope :with_artifacts, ->() do
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id')) '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end end
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_stored_locally, -> { with_artifacts.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) } scope :ref_protected, -> { where(protected: true) }
......
...@@ -531,7 +531,7 @@ module Ci ...@@ -531,7 +531,7 @@ module Ci
# We purposely cast the builds to an Array here. Because we always use the # We purposely cast the builds to an Array here. Because we always use the
# rows if there are more than 0 this prevents us from having to run two # rows if there are more than 0 this prevents us from having to run two
# queries: one to get the count and one to get the rows. # queries: one to get the count and one to get the rows.
@latest_builds_with_artifacts ||= builds.latest.with_artifacts.to_a @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
end end
private private
......
...@@ -136,7 +136,7 @@ module Clusters ...@@ -136,7 +136,7 @@ module Clusters
kubeclient = build_kubeclient! kubeclient = build_kubeclient!
kubeclient.get_pods(namespace: actual_namespace).as_json kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err rescue Kubeclient::HttpError => err
raise err unless err.error_code == 404 raise err unless err.error_code == 404
[] []
......
...@@ -554,7 +554,7 @@ class Project < ActiveRecord::Base ...@@ -554,7 +554,7 @@ class Project < ActiveRecord::Base
latest_pipeline = pipelines.latest_successful_for(ref) latest_pipeline = pipelines.latest_successful_for(ref)
if latest_pipeline if latest_pipeline
latest_pipeline.builds.latest.with_artifacts latest_pipeline.builds.latest.with_artifacts_archive
else else
builds.none builds.none
end end
...@@ -1534,8 +1534,8 @@ class Project < ActiveRecord::Base ...@@ -1534,8 +1534,8 @@ class Project < ActiveRecord::Base
@errors = original_errors @errors = original_errors
end end
def add_export_job(current_user:) def add_export_job(current_user:, params: {})
job_id = ProjectExportWorker.perform_async(current_user.id, self.id) job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
if job_id if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}" Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
......
...@@ -161,11 +161,6 @@ class JiraService < IssueTrackerService ...@@ -161,11 +161,6 @@ class JiraService < IssueTrackerService
add_comment(data, jira_issue) add_comment(data, jira_issue)
end end
# reason why service cannot be tested
def disabled_title
"Please fill in Password and Username."
end
def test(_) def test(_)
result = test_settings result = test_settings
success = result.present? success = result.present?
......
...@@ -199,7 +199,7 @@ class KubernetesService < DeploymentService ...@@ -199,7 +199,7 @@ class KubernetesService < DeploymentService
kubeclient = build_kubeclient! kubeclient = build_kubeclient!
kubeclient.get_pods(namespace: actual_namespace).as_json kubeclient.get_pods(namespace: actual_namespace).as_json
rescue KubeException => err rescue Kubeclient::HttpError => err
raise err unless err.error_code == 404 raise err unless err.error_code == 404
[] []
......
...@@ -39,10 +39,6 @@ class PipelinesEmailService < Service ...@@ -39,10 +39,6 @@ class PipelinesEmailService < Service
project.pipelines.any? project.pipelines.any?
end end
def disabled_title
'Please setup a pipeline on your repository.'
end
def test_data(project, user) def test_data(project, user)
data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last) data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
data[:user] = user.hook_attrs data[:user] = user.hook_attrs
......
...@@ -163,11 +163,6 @@ class Service < ActiveRecord::Base ...@@ -163,11 +163,6 @@ class Service < ActiveRecord::Base
true true
end end
# reason why service cannot be tested
def disabled_title
"Please setup a project repository."
end
# Provide convenient accessor methods # Provide convenient accessor methods
# for each serialized property. # for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty # Also keep track of updated properties in a similar way as ActiveModel::Dirty
......
...@@ -32,7 +32,7 @@ module Ci ...@@ -32,7 +32,7 @@ module Ci
kubeclient = build_kubeclient! kubeclient = build_kubeclient!
kubeclient.get_secrets.as_json kubeclient.get_secrets.as_json
rescue KubeException => err rescue Kubeclient::HttpError => err
raise err unless err.error_code == 404 raise err unless err.error_code == 404
[] []
......
...@@ -12,7 +12,7 @@ module Clusters ...@@ -12,7 +12,7 @@ module Clusters
else else
check_timeout check_timeout
end end
rescue KubeException => ke rescue Kubeclient::HttpError => ke
app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored? app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored?
end end
......
...@@ -10,7 +10,7 @@ module Clusters ...@@ -10,7 +10,7 @@ module Clusters
ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue KubeException => ke rescue Kubeclient::HttpError => ke
app.make_errored!("Kubernetes error: #{ke.message}") app.make_errored!("Kubernetes error: #{ke.message}")
rescue StandardError rescue StandardError
app.make_errored!("Can't start installation process") app.make_errored!("Can't start installation process")
......
module Files module Files
class CreateService < Files::BaseService class CreateService < Files::BaseService
def create_commit! def create_commit!
handler = Lfs::FileModificationHandler.new(project, @branch_name) transformer = Lfs::FileTransformer.new(project, @branch_name)
handler.new_file(@file_path, @file_content) do |content_or_lfs_pointer| result = transformer.new_file(@file_path, @file_content)
create_transformed_commit(content_or_lfs_pointer)
end create_transformed_commit(result.content)
end end
private private
......
...@@ -3,11 +3,33 @@ module Files ...@@ -3,11 +3,33 @@ module Files
UPDATE_FILE_ACTIONS = %w(update move delete).freeze UPDATE_FILE_ACTIONS = %w(update move delete).freeze
def create_commit! def create_commit!
transformer = Lfs::FileTransformer.new(project, @branch_name)
actions = actions_after_lfs_transformation(transformer, params[:actions])
commit_actions!(actions)
end
private
def actions_after_lfs_transformation(transformer, actions)
actions.map do |action|
if action[:action] == 'create'
result = transformer.new_file(action[:file_path], action[:content], encoding: action[:encoding])
action[:content] = result.content
action[:encoding] = result.encoding
end
action
end
end
def commit_actions!(actions)
repository.multi_action( repository.multi_action(
current_user, current_user,
message: @commit_message, message: @commit_message,
branch_name: @branch_name, branch_name: @branch_name,
actions: params[:actions], actions: actions,
author_email: @author_email, author_email: @author_email,
author_name: @author_name, author_name: @author_name,
start_project: @start_project, start_project: @start_project,
...@@ -17,8 +39,6 @@ module Files ...@@ -17,8 +39,6 @@ module Files
raise_error(e) raise_error(e)
end end
private
def validate! def validate!
super super
......
module Lfs module Lfs
class FileModificationHandler # Usage: Calling `new_file` check to see if a file should be in LFS and
# return a transformed result with `content` and `encoding` to commit.
#
# For LFS an LfsObject linked to the project is stored and an LFS
# pointer returned. If the file isn't in LFS the untransformed content
# is returned to save in the commit.
#
# transformer = Lfs::FileTransformer.new(project, @branch_name)
# content_or_lfs_pointer = transformer.new_file(file_path, content).content
# create_transformed_commit(content_or_lfs_pointer)
#
class FileTransformer
attr_reader :project, :branch_name attr_reader :project, :branch_name
delegate :repository, to: :project delegate :repository, to: :project
...@@ -9,24 +20,37 @@ module Lfs ...@@ -9,24 +20,37 @@ module Lfs
@branch_name = branch_name @branch_name = branch_name
end end
def new_file(file_path, file_content) def new_file(file_path, file_content, encoding: nil)
if project.lfs_enabled? && lfs_file?(file_path) if project.lfs_enabled? && lfs_file?(file_path)
file_content = Base64.decode64(file_content) if encoding == 'base64'
lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content) lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
lfs_object = create_lfs_object!(lfs_pointer_file, file_content) lfs_object = create_lfs_object!(lfs_pointer_file, file_content)
content = lfs_pointer_file.pointer
success = yield(content) link_lfs_object!(lfs_object)
link_lfs_object!(lfs_object) if success Result.new(content: lfs_pointer_file.pointer, encoding: 'text')
else else
yield(file_content) Result.new(content: file_content, encoding: encoding)
end
end
class Result
attr_reader :content, :encoding
def initialize(content:, encoding:)
@content = content
@encoding = encoding
end end
end end
private private
def lfs_file?(file_path) def lfs_file?(file_path)
repository.attributes_at(branch_name, file_path)['filter'] == 'lfs' cached_attributes.attributes(file_path)['filter'] == 'lfs'
end
def cached_attributes
@cached_attributes ||= Gitlab::Git::AttributesAtRefParser.new(repository, branch_name)
end end
def create_lfs_object!(lfs_pointer_file, file_content) def create_lfs_object!(lfs_pointer_file, file_content)
......
...@@ -26,7 +26,7 @@ module Projects ...@@ -26,7 +26,7 @@ module Projects
end end
def project_tree_saver 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 end
def uploads_saver def uploads_saver
......
...@@ -15,11 +15,6 @@ ...@@ -15,11 +15,6 @@
.footer-block.row-content-block .footer-block.row-content-block
= service_save_button(@service) = service_save_button(@service)
&nbsp; &nbsp;
- if @service.valid? && @service.activated?
- unless @service.can_test?
- disabled_class = 'disabled'
- disabled_title = @service.disabled_title
= link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel' = link_to 'Cancel', project_settings_integrations_path(@project), class: 'btn btn-cancel'
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true) - if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
......
...@@ -4,10 +4,11 @@ class ProjectExportWorker ...@@ -4,10 +4,11 @@ class ProjectExportWorker
sidekiq_options retry: 3 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) current_user = User.find(current_user_id)
project = Project.find(project_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
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: Update knapsack to 1.16.0
merge_request: 17735
author: Takuya Noguchi
type: other
---
title: Fix file upload on project show page
merge_request:
author:
type: fixed
---
title: Create commit API and Web IDE obey LFS filters
merge_request: 16718
author:
type: fixed
---
title: Move UnresolvedDiscussions vue component
merge_request: 17538
author: George Tsiolis
type: performance
---
title: Cache column_exists? for application settings
merge_request:
author:
type: performance
---
title: Cache table_exists?('application_settings') to reduce repeated schema reloads
merge_request:
author:
type: performance
---
title: Fix timeouts loading /admin/projects page
merge_request:
author:
type: performance
---
title: Update CI services documnetation
merge_request: 17749
author:
type: other
...@@ -47,6 +47,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. ...@@ -47,6 +47,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
[source installations](../install/installation.md#installation-from-source). [source installations](../install/installation.md#installation-from-source).
- [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab. - [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
- **(Starter/Premium)** [Elasticsearch](../integration/elasticsearch.md): Enable Elasticsearch to empower GitLab's Advanced Global Search. Useful when you deal with a huge amount of data. - **(Starter/Premium)** [Elasticsearch](../integration/elasticsearch.md): Enable Elasticsearch to empower GitLab's Advanced Global Search. Useful when you deal with a huge amount of data.
- **(Premium)** [External Classification Policy Authorization](../user/admin_area/settings/external_authorization.md)
#### Customizing GitLab's appearance #### Customizing GitLab's appearance
......
...@@ -84,9 +84,9 @@ checks using those checksums can be run. These checks also detect missing files. ...@@ -84,9 +84,9 @@ checks using those checksums can be run. These checks also detect missing files.
Currently, integrity checks are supported for the following types of file: Currently, integrity checks are supported for the following types of file:
* CI artifacts * CI artifacts (Available from version 10.7.0)
* LFS objects * LFS objects (Available from version 10.6.0)
* User uploads * User uploads (Available from version 10.6.0)
**Omnibus Installation** **Omnibus Installation**
......
...@@ -15,9 +15,10 @@ POST /projects/:id/export ...@@ -15,9 +15,10 @@ POST /projects/:id/export
| Attribute | Type | Required | Description | | 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 | | `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 ```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 ```json
......
...@@ -170,6 +170,9 @@ PUT /application/settings ...@@ -170,6 +170,9 @@ PUT /application/settings
| `user_default_external` | boolean | no | Newly registered users will by default be external | | `user_default_external` | boolean | no | Newly registered users will by default be external |
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider | | `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
| `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | | `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. |
| `external_authorization_service_enabled` | boolean | no | Enable using an external authorization service for accessing projects |
| `external_authorization_service_url` | string | no | URL to which authorization requests will be directed |
| `external_authorization_service_default_label` | string | no | The default classification label to use when requesting authorization and no classification label has been specified on the project |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
...@@ -207,6 +210,9 @@ Example response: ...@@ -207,6 +210,9 @@ Example response:
"plantuml_enabled": false, "plantuml_enabled": false,
"plantuml_url": null, "plantuml_url": null,
"terminal_max_session_time": 0, "terminal_max_session_time": 0,
"polling_interval_multiplier": 1.0 "polling_interval_multiplier": 1.0,
"external_authorization_service_enabled": true,
"external_authorization_service_url": "https://authorize.me",
"external_authorization_service_default_label": "default"
} }
``` ```
...@@ -58,7 +58,7 @@ your job and is linked to the Docker image that the `image` keyword defines. ...@@ -58,7 +58,7 @@ your job and is linked to the Docker image that the `image` keyword defines.
This allows you to access the service image during build time. This allows you to access the service image during build time.
The service image can run any application, but the most common use case is to The service image can run any application, but the most common use case is to
run a database container, eg. `mysql`. It's easier and faster to use an run a database container, e.g., `mysql`. It's easier and faster to use an
existing image and run it as an additional container than install `mysql` every existing image and run it as an additional container than install `mysql` every
time the project is built. time the project is built.
...@@ -83,6 +83,67 @@ So, in order to access your database service you have to connect to the host ...@@ -83,6 +83,67 @@ So, in order to access your database service you have to connect to the host
named `mysql` instead of a socket or `localhost`. Read more in [accessing the named `mysql` instead of a socket or `localhost`. Read more in [accessing the
services](#accessing-the-services). services](#accessing-the-services).
### How the health check of services works
Services are designed to provide additional functionality which is **network accessible**.
It may be a database like MySQL, or Redis, and even `docker:dind` which
allows you to use Docker in Docker. It can be practically anything that is
required for the CI/CD job to proceed and is accessed by network.
To make sure this works, the Runner:
1. checks which ports are exposed from the container by default
1. starts a special container that waits for these ports to be accessible
When the second stage of the check fails, either because there is no opened port in the
service, or the service was not started properly before the timeout and the port is not
responding, it prints the warning: `*** WARNING: Service XYZ probably didn't start properly`.
In most cases it will affect the job, but there may be situations when the job
will still succeed even if that warning was printed. For example:
- The service was started a little after the warning was raised, and the job is
not using the linked service from the very beginning. In that case, when the
job needed to access the service, it may have been already there waiting for
connections.
- The service container is not providing any networking service, but it's doing
something with the job's directory (all services have the job directory mounted
as a volume under `/builds`). In that case, the service will do its job, and
since the job is not trying to connect to it, it won't fail.
### What services are not for
As it was mentioned before, this feature is designed to provide **network accessible**
services. A database is the simplest example of such a service.
NOTE: **Note:**
The services feature is not designed to, and will not add any software from the
defined `services` image(s) to the job's container.
For example, if you have the following `services` defined in your job, the `php`,
`node` or `go` commands will **not** be available for your script, and thus
the job will fail:
```yaml
job:
services:
- php:7
- node:latest
- golang:1.10
image: alpine:3.7
script:
- php -v
- node -v
- go version
```
If you need to have `php`, `node` and `go` available for your script, you should
either:
- choose an existing Docker image that contains all required tools, or
- create your own Docker image, which will have all the required tools included
and use that in your job
### Accessing the services ### Accessing the services
Let's say that you need a Wordpress instance to test some API integration with Let's say that you need a Wordpress instance to test some API integration with
......
...@@ -35,7 +35,7 @@ Starting with [GitLab Starter][ee] 9.3, this information will ...@@ -35,7 +35,7 @@ Starting with [GitLab Starter][ee] 9.3, this information will
be automatically extracted and shown right in the merge request widget. To do be automatically extracted and shown right in the merge request widget. To do
so, the CI/CD job must be named `codequality` and the artifact path must be so, the CI/CD job must be named `codequality` and the artifact path must be
`codeclimate.json`. `codeclimate.json`.
[Learn more on code quality diffs in merge requests](../../user/project/merge_requests/code_quality_diff.md). [Learn more on code quality diffs in merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html).
[cli]: https://github.com/codeclimate/codeclimate [cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor [dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
......
...@@ -22,7 +22,7 @@ You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pip ...@@ -22,7 +22,7 @@ You can find these nightly pipelines at [GitLab QA pipelines page][gitlab-qa-pip
It is possible to run end-to-end tests (eventually being run within a It is possible to run end-to-end tests (eventually being run within a
[GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering [GitLab QA pipeline][gitlab-qa-pipelines]) for a merge request by triggering
the `package-qa` manual action, that should be present in a merge request the `package-and-qa` manual action, that should be present in a merge request
widget. widget.
Manual action that starts end-to-end tests is also available in merge requests Manual action that starts end-to-end tests is also available in merge requests
......
...@@ -303,7 +303,10 @@ a couple more commits to the EE branch, but ask the reviewer to review the EE me ...@@ -303,7 +303,10 @@ a couple more commits to the EE branch, but ask the reviewer to review the EE me
additionally to the CE MR. If there are many EE-only changes though, start a new MR additionally to the CE MR. If there are many EE-only changes though, start a new MR
to EE only. to EE only.
### Previewing the changes live ## Previewing the changes live
To preview your changes to documentation locally, please follow
this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
To preview your changes to documentation locally, please follow To preview your changes to documentation locally, please follow
this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development). this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development).
......
...@@ -70,7 +70,7 @@ To downgrade an Omnibus installation, it is sufficient to install the Community ...@@ -70,7 +70,7 @@ To downgrade an Omnibus installation, it is sufficient to install the Community
Edition package on top of the currently installed one. You can do this manually, Edition package on top of the currently installed one. You can do this manually,
by directly [downloading the package](https://packages.gitlab.com/gitlab/gitlab-ce) by directly [downloading the package](https://packages.gitlab.com/gitlab/gitlab-ce)
you need, or by adding our CE package repository and following the you need, or by adding our CE package repository and following the
[CE installation instructions](https://about.gitlab.com/downloads/?version=ce). [CE installation instructions](https://about.gitlab.com/installation/?version=ce).
**Source Installation** **Source Installation**
......
...@@ -464,7 +464,9 @@ bother us. In any case, it is something to keep in mind when deploying GitLab ...@@ -464,7 +464,9 @@ bother us. In any case, it is something to keep in mind when deploying GitLab
on a production cluster. on a production cluster.
In order to deploy GitLab on a production cluster, you will need to assign the 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: 1. Edit the Security Context:
```sh ```sh
...@@ -477,6 +479,12 @@ GitLab service account to the `anyuid` Security Context. ...@@ -477,6 +479,12 @@ GitLab service account to the `anyuid` Security Context.
1. Save and exit the editor 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 ## Conclusion
By now, you should have an understanding of the basic OpenShift Origin concepts 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. ...@@ -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" [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" [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" [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
# External authorization control
>
[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/4216) in
[GitLab Premium](https://about.gitlab.com/pricing) 10.6.
In highly controlled environments, it may be necessary for access policy to be
controlled by an external service that permits access based on project
classification and user access. GitLab provides a way to check project
authorization with your own defined service.
## Overview
Once the external service is configured and enabled, when a project is accessed,
a request is made to the external service with the user information and project
classification label assigned to the project. When the service replies with a
known response, the result is cached for 6 hours.
If the external authorization is enabled, GitLab will further block pages and
functionality that render cross-project data. That includes:
- most pages under Dashboard (Activity, Milestones, Snippets, Assigned merge
requests, Assigned issues, Todos)
- under a specific group (Activity, Contribution analytics, Issues, Issue boards,
Labels, Milestones, Merge requests)
- Global and Group search will be disabled
This is to prevent performing to many requests at once to the external
authorization service.
## Configuration
The external authorization service can be enabled by an admin on the GitLab's
admin area under the settings page:
![Enable external authorization service](img/external_authorization_service_settings.png)
The available required properties are:
- **Service URL**: The URL to make authorization requests to
- **Default classification label**: The classification label to use when
requesting authorization if no specific label is defined on the project
## How it works
When GitLab requests access, it will send a JSON POST request to the external
service with this body:
```json
{
"user_identifier": "jane@acme.org",
"project_classification_label": "project-label",
"user_ldap_dn": "CN=Jane Doe,CN=admin,DC=acme"
}
```
The `user_ldap_dn` is optional and is only sent when the user is logged in
through LDAP.
When the external authorization service responds with a status code 200, the
user is granted access. When the external service responds with a status code
401, the user is denied access. In any case, the request is cached for 6 hours.
When denying access, a `reason` can be optionally specified in the JSON body:
```json
{
"reason": "You are not allowed access to this project."
}
```
Any other status code than 401 or 200 will also deny access to the user, but the
response will not be cached.
If the service times out (after 500ms), a message "External Policy Server did
not respond" will be displayed.
## Classification labels
You can use your own classification label in the project's
**Settings > General > General project settings** page in the "Classification
label" box. When no classification label is specified on a project, the default
label defined in the [global settings](#configuration) will be used.
The label will be shown on all project pages in the upper right corner.
![classification label on project page](img/classification_label_on_project_page.png)
...@@ -53,6 +53,22 @@ To get started with the command line, please read through the ...@@ -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. 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 ## Branches
When you submit changes in a new [branch](branches/index.md), you create a new version 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. ...@@ -158,3 +174,5 @@ Lock your files to prevent any conflicting changes.
## Repository's API ## Repository's API
You can access your repos via [repository API](../../../api/repositories.md). You can access your repos via [repository API](../../../api/repositories.md).
[jupyter]: https://jupyter.org
...@@ -24,8 +24,11 @@ ...@@ -24,8 +24,11 @@
methods: { methods: {
...mapActions([ ...mapActions([
'discardFileChanges', 'discardFileChanges',
'updateViewer',
]), ]),
openFileInEditor(file) { openFileInEditor(file) {
this.updateViewer('diff');
router.push(`/project${file.url}`); router.push(`/project${file.url}`);
}, },
}, },
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script>
<template>
<div
class="dropdown"
:class="{
shadow: showShadow,
}"
>
<button
type="button"
class="btn btn-primary btn-sm"
:class="{
'btn-inverted': hasChanges,
}"
data-toggle="dropdown"
>
<template v-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
{{ __('Reviewing') }}
</template>
<icon
name="angle-down"
:size="12"
css-classes="caret-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<li>
<a
href="#"
@click.prevent="changeMode('editor')"
:class="{
'is-active': viewer === 'editor',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Editing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('View and edit lines') }}
</span>
</a>
</li>
<li>
<a
href="#"
@click.prevent="changeMode('diff')"
:class="{
'is-active': viewer === 'diff',
}"
>
<strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the last commit') }}
</span>
</a>
</li>
</ul>
</div>
</div>
</template>
<script>
import icon from '~/vue_shared/components/icon.vue';
export default {
components: {
icon,
},
props: {
projectUrl: {
type: String,
required: true,
},
},
computed: {
goBackUrl() {
return document.referrer || this.projectUrl;
},
},
};
</script>
<template>
<nav
class="ide-external-links"
v-once
>
<p>
<a
:href="goBackUrl"
class="ide-sidebar-link"
>
<icon
:size="16"
class="append-right-8"
name="go-back"
/>
<span class="ide-external-links-text">
{{ s__('Go back') }}
</span>
</a>
</p>
</nav>
</template>
<script> <script>
import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; import projectAvatarImage from '~/vue_shared/components/project_avatar/image.vue';
import branchesTree from './ide_project_branches_tree.vue'; import branchesTree from './ide_project_branches_tree.vue';
import externalLinks from './ide_external_links.vue';
export default { export default {
components: { components: {
branchesTree, branchesTree,
externalLinks,
projectAvatarImage, projectAvatarImage,
}, },
props: { props: {
...@@ -37,6 +39,9 @@ export default { ...@@ -37,6 +39,9 @@ export default {
</div> </div>
</a> </a>
</div> </div>
<external-links
:project-url="project.web_url"
/>
<div class="multi-file-commit-panel-inner-scroll"> <div class="multi-file-commit-panel-inner-scroll">
<branches-tree <branches-tree
v-for="branch in project.branches" v-for="branch in project.branches"
......
...@@ -15,6 +15,8 @@ export default { ...@@ -15,6 +15,8 @@ export default {
'leftPanelCollapsed', 'leftPanelCollapsed',
'rightPanelCollapsed', 'rightPanelCollapsed',
'panelResizing', 'panelResizing',
'viewer',
'delayViewerUpdated',
]), ]),
shouldHideEditor() { shouldHideEditor() {
return this.activeFile && this.activeFile.binary && !this.activeFile.raw; return this.activeFile && this.activeFile.binary && !this.activeFile.raw;
...@@ -37,6 +39,9 @@ export default { ...@@ -37,6 +39,9 @@ export default {
this.editor.updateDimensions(); this.editor.updateDimensions();
} }
}, },
viewer() {
this.createEditorInstance();
},
}, },
beforeDestroy() { beforeDestroy() {
this.editor.dispose(); this.editor.dispose();
...@@ -59,6 +64,8 @@ export default { ...@@ -59,6 +64,8 @@ export default {
'setFileLanguage', 'setFileLanguage',
'setEditorPosition', 'setEditorPosition',
'setFileEOL', 'setFileEOL',
'updateViewer',
'updateDelayViewerUpdated',
]), ]),
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
...@@ -67,16 +74,34 @@ export default { ...@@ -67,16 +74,34 @@ export default {
this.getRawFileData(this.activeFile) this.getRawFileData(this.activeFile)
.then(() => { .then(() => {
this.editor.createInstance(this.$refs.editor); const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
return viewerPromise;
})
.then(() => {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
}) })
.then(() => this.setupEditor())
.catch((err) => { .catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err; throw err;
}); });
}, },
createEditorInstance() {
this.editor.dispose();
this.$nextTick(() => {
if (this.viewer === 'editor') {
this.editor.createInstance(this.$refs.editor);
} else {
this.editor.createDiffInstance(this.$refs.editor);
}
this.setupEditor();
});
},
setupEditor() { setupEditor() {
if (!this.activeFile) return; if (!this.activeFile || !this.editor.instance) return;
this.model = this.editor.createModel(this.activeFile); this.model = this.editor.createModel(this.activeFile);
......
<script> <script>
import { mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
...@@ -70,6 +70,9 @@ ...@@ -70,6 +70,9 @@
} }
}, },
methods: { methods: {
...mapActions([
'updateDelayViewerUpdated',
]),
clickFile(row) { clickFile(row) {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) { if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
...@@ -78,7 +81,13 @@ ...@@ -78,7 +81,13 @@
tree: this.file, tree: this.file,
}); });
} }
const delayPromise = this.file.changed ?
Promise.resolve() : this.updateDelayViewerUpdated(true);
return delayPromise.then(() => {
this.$router.push(`/project${row.url}`); this.$router.push(`/project${row.url}`);
});
}, },
}, },
}; };
......
<script> <script>
import { mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
export default { export default {
components: { components: {
'repo-tab': RepoTab, RepoTab,
EditorMode,
},
data() {
return {
showShadow: false,
};
}, },
computed: { computed: {
...mapGetters([
'hasChanges',
]),
...mapState([ ...mapState([
'openFiles', 'openFiles',
'viewer',
]),
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions([
'updateViewer',
]), ]),
}, },
}; };
</script> </script>
<template> <template>
<div class="multi-file-tabs">
<ul <ul
class="multi-file-tabs list-unstyled append-bottom-0" class="list-unstyled append-bottom-0"
ref="tabsScroller"
> >
<repo-tab <repo-tab
v-for="tab in openFiles" v-for="tab in openFiles"
...@@ -24,4 +47,11 @@ ...@@ -24,4 +47,11 @@
:tab="tab" :tab="tab"
/> />
</ul> </ul>
<editor-mode
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
@click="updateViewer"
/>
</div>
</template> </template>
...@@ -26,6 +26,9 @@ export default class Model { ...@@ -26,6 +26,9 @@ export default class Model {
this.events = new Map(); this.events = new Map();
this.updateContent = this.updateContent.bind(this); this.updateContent = this.updateContent.bind(this);
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent); eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
...@@ -75,6 +78,7 @@ export default class Model { ...@@ -75,6 +78,7 @@ export default class Model {
this.disposable.dispose(); this.disposable.dispose();
this.events.clear(); this.events.clear();
eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent); eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
} }
} }
import eventHub from 'ee/ide/eventhub';
import Disposable from './disposable'; import Disposable from './disposable';
import Model from './model'; import Model from './model';
...@@ -25,9 +26,17 @@ export default class ModelManager { ...@@ -25,9 +26,17 @@ export default class ModelManager {
this.models.set(model.path, model); this.models.set(model.path, model);
this.disposable.add(model); this.disposable.add(model);
eventHub.$on(`editor.update.model.dispose.${file.path}`, this.removeCachedModel.bind(this, file));
return model; return model;
} }
removeCachedModel(file) {
this.models.delete(file.path);
eventHub.$off(`editor.update.model.dispose.${file.path}`, this.removeCachedModel);
}
dispose() { dispose() {
// dispose of all the models // dispose of all the models
this.disposable.dispose(); this.disposable.dispose();
......
...@@ -27,6 +27,8 @@ export default class DecorationsController { ...@@ -27,6 +27,8 @@ export default class DecorationsController {
} }
decorate(model) { decorate(model) {
if (!this.editor.instance) return;
const decorations = this.getAllDecorationsForModel(model); const decorations = this.getAllDecorationsForModel(model);
const oldDecorations = this.editorDecorations.get(model.url) || []; const oldDecorations = this.editorDecorations.get(model.url) || [];
......
...@@ -3,9 +3,16 @@ import DecorationsController from './decorations/controller'; ...@@ -3,9 +3,16 @@ import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller'; import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable'; import Disposable from './common/disposable';
import ModelManager from './common/model_manager'; import ModelManager from './common/model_manager';
import editorOptions from './editor_options'; import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme';
import gitlabTheme from 'ee/ide/lib/themes/gl_theme'; // eslint-disable-line import/first export const clearDomElement = el => {
if (!el || !el.firstChild) return;
while (el.firstChild) {
el.removeChild(el.firstChild);
}
};
export default class Editor { export default class Editor {
static create(monaco) { static create(monaco) {
...@@ -34,19 +41,31 @@ export default class Editor { ...@@ -34,19 +41,31 @@ export default class Editor {
createInstance(domElement) { createInstance(domElement) {
if (!this.instance) { if (!this.instance) {
clearDomElement(domElement);
this.disposable.add( this.disposable.add(
this.instance = this.monaco.editor.create(domElement, { (this.instance = this.monaco.editor.create(domElement, {
model: null, ...defaultEditorOptions,
readOnly: false, })),
contextmenu: true, (this.dirtyDiffController = new DirtyDiffController(
scrollBeyondLastLine: false, this.modelManager,
minimap: { this.decorationsController,
enabled: false, )),
}, );
}),
this.dirtyDiffController = new DirtyDiffController( window.addEventListener('resize', this.debouncedUpdate, false);
this.modelManager, this.decorationsController, }
), }
createDiffInstance(domElement) {
if (!this.instance) {
clearDomElement(domElement);
this.disposable.add(
(this.instance = this.monaco.editor.createDiffEditor(domElement, {
...defaultEditorOptions,
readOnly: true,
})),
); );
window.addEventListener('resize', this.debouncedUpdate, false); window.addEventListener('resize', this.debouncedUpdate, false);
...@@ -58,25 +77,39 @@ export default class Editor { ...@@ -58,25 +77,39 @@ export default class Editor {
} }
attachModel(model) { attachModel(model) {
if (this.instance.getEditorType() === 'vs.editor.IDiffEditor') {
this.instance.setModel({
original: model.getOriginalModel(),
modified: model.getModel(),
});
return;
}
this.instance.setModel(model.getModel()); this.instance.setModel(model.getModel());
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model); if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model; this.currentModel = model;
this.instance.updateOptions(editorOptions.reduce((acc, obj) => { this.instance.updateOptions(
Object.keys(obj).forEach((key) => { editorOptions.reduce((acc, obj) => {
Object.keys(obj).forEach(key => {
Object.assign(acc, { Object.assign(acc, {
[key]: obj[key](model), [key]: obj[key](model),
}); });
}); });
return acc; return acc;
}, {})); }, {}),
);
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
} }
setupMonacoTheme() { setupMonacoTheme() {
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); this.monaco.editor.defineTheme(
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.setTheme('gitlab'); this.monaco.editor.setTheme('gitlab');
} }
...@@ -88,12 +121,21 @@ export default class Editor { ...@@ -88,12 +121,21 @@ export default class Editor {
} }
dispose() { dispose() {
this.disposable.dispose();
window.removeEventListener('resize', this.debouncedUpdate); window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance // catch any potential errors with disposing the error
if (this.instance) { // this is mainly for tests caused by elements not existing
try {
this.disposable.dispose();
this.instance = null;
} catch (e) {
this.instance = null; this.instance = null;
if (process.env.NODE_ENV !== 'test') {
// eslint-disable-next-line no-console
console.error(e);
}
} }
} }
...@@ -113,6 +155,8 @@ export default class Editor { ...@@ -113,6 +155,8 @@ export default class Editor {
} }
onPositionChange(cb) { onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add( this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)), this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
); );
......
export default [{ export const defaultEditorOptions = {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
minimap: {
enabled: false,
},
};
export default [
{
readOnly: model => !!model.file.file_lock, readOnly: model => !!model.file.file_lock,
}]; },
];
...@@ -6,6 +6,9 @@ export default { ...@@ -6,6 +6,9 @@ export default {
rules: [], rules: [],
colors: { colors: {
'editorLineNumber.foreground': '#CCCCCC', 'editorLineNumber.foreground': '#CCCCCC',
'diffEditor.insertedTextBackground': '#ddfbe6',
'diffEditor.removedTextBackground': '#f9d7dc',
'editor.selectionBackground': '#aad6f8',
}, },
}, },
}; };
...@@ -84,6 +84,14 @@ export const scrollToTab = () => { ...@@ -84,6 +84,14 @@ export const scrollToTab = () => {
}); });
}; };
export const updateViewer = ({ commit }, viewer) => {
commit(types.UPDATE_VIEWER, viewer);
};
export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
};
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
import { normalizeHeaders } from '~/lib/utils/common_utils'; import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash'; import flash from '~/flash';
import eventHub from 'ee/ide/eventhub';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import router from '../../ide_router'; import router from '../../ide_router';
...@@ -27,6 +28,8 @@ export const closeFile = ({ commit, state, dispatch }, file) => { ...@@ -27,6 +28,8 @@ export const closeFile = ({ commit, state, dispatch }, file) => {
} }
dispatch('getLastCommitData'); dispatch('getLastCommitData');
eventHub.$emit(`editor.update.model.dispose.${file.path}`);
}; };
export const setFileActive = ({ commit, state, getters, dispatch }, file) => { export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
...@@ -150,4 +153,6 @@ export const discardFileChanges = ({ commit }, file) => { ...@@ -150,4 +153,6 @@ export const discardFileChanges = ({ commit }, file) => {
if (file.tempFile && file.opened) { if (file.tempFile && file.opened) {
commit(types.TOGGLE_FILE_OPEN, file); commit(types.TOGGLE_FILE_OPEN, file);
} }
eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
}; };
...@@ -15,3 +15,5 @@ export const canEditFile = (state) => { ...@@ -15,3 +15,5 @@ export const canEditFile = (state) => {
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile); export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const hasChanges = state => !!state.changedFiles.length;
...@@ -46,3 +46,6 @@ export const SET_EDIT_MODE = 'SET_EDIT_MODE'; ...@@ -46,3 +46,6 @@ export const SET_EDIT_MODE = 'SET_EDIT_MODE';
export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE'; export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
...@@ -57,6 +57,16 @@ export default { ...@@ -57,6 +57,16 @@ export default {
lastCommitMsg, lastCommitMsg,
}); });
}, },
[types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, {
viewer,
});
},
[types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) {
Object.assign(state, {
delayViewerUpdated,
});
},
...projectMutations, ...projectMutations,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
......
...@@ -20,4 +20,6 @@ export default () => ({ ...@@ -20,4 +20,6 @@ export default () => ({
leftPanelCollapsed: false, leftPanelCollapsed: false,
rightPanelCollapsed: false, rightPanelCollapsed: false,
panelResizing: false, panelResizing: false,
viewer: 'editor',
delayViewerUpdated: false,
}); });
...@@ -240,10 +240,10 @@ export default { ...@@ -240,10 +240,10 @@ export default {
}, },
fetchDockerReport() { fetchDockerReport() {
const { path } = this.mr.sastContainer; const { head_path } = this.mr.sastContainer;
this.isLoadingDocker = true; this.isLoadingDocker = true;
this.service.fetchReport(path) this.service.fetchReport(head_path)
.then((data) => { .then((data) => {
this.mr.setDockerReport(data); this.mr.setDockerReport(data);
this.isLoadingDocker = false; this.isLoadingDocker = false;
...@@ -257,7 +257,7 @@ export default { ...@@ -257,7 +257,7 @@ export default {
fetchDastReport() { fetchDastReport() {
this.isLoadingDast = true; this.isLoadingDast = true;
this.service.fetchReport(this.mr.dast.path) this.service.fetchReport(this.mr.dast.head_path)
.then((data) => { .then((data) => {
this.mr.setDastReport(data); this.mr.setDastReport(data);
this.isLoadingDast = false; this.isLoadingDast = false;
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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