Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzegorz@gitlab.com>2018-03-17 08:52:35 +0300
committerGrzegorz Bizon <grzegorz@gitlab.com>2018-03-17 08:52:35 +0300
commit83874edb77235d2a276f114f72942c43e8be3a44 (patch)
tree890ac40129ec2fadda1e2e3ad609cdc9a7605537 /app
parent53e2987ba6a8b2fb79f5754ae13924f2939d81fd (diff)
parentea5221aeb358ef6c349cfa09b9c6993bd7bd027d (diff)
Merge branch 'master' into 'update-kubeclient'
Conflicts: Gemfile.lock
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js3
-rw-r--r--app/assets/javascripts/merge_request_tabs.js5
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue1
-rw-r--r--app/assets/javascripts/mr_notes/index.js9
-rw-r--r--app/assets/javascripts/notes.js726
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue551
-rw-r--r--app/assets/javascripts/notes/components/diff_file_header.vue34
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue96
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue114
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue18
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue226
-rw-r--r--app/assets/javascripts/notes/components/note_attachment.vue16
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue349
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue134
-rw-r--r--app/assets/javascripts/notes/components/note_edited_text.vue50
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue224
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue110
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue24
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue360
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue260
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue282
-rw-r--r--app/assets/javascripts/notes/index.js68
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js6
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js11
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js4
-rw-r--r--app/assets/javascripts/notes/stores/actions.js266
-rw-r--r--app/assets/javascripts/notes/stores/getters.js34
-rw-r--r--app/assets/javascripts/notes/stores/index.js3
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js32
-rw-r--r--app/assets/javascripts/notes/stores/utils.js13
-rw-r--r--app/assets/javascripts/pages/ci/lints/new/index.js (renamed from app/assets/javascripts/pages/ci/lints/create/index.js)0
-rw-r--r--app/assets/javascripts/pages/profiles/notifications/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js22
-rw-r--r--app/assets/javascripts/performance_bar.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue144
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js113
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js16
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss35
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/assets/stylesheets/pages/wiki.scss5
-rw-r--r--app/controllers/projects/discussions_controller.rb6
-rw-r--r--app/finders/admin/projects_finder.rb3
-rw-r--r--app/helpers/javascript_helper.rb5
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/ci/build.rb137
-rw-r--r--app/models/ci/pipeline.rb9
-rw-r--r--app/models/ci/runner.rb9
-rw-r--r--app/models/clusters/platforms/kubernetes.rb24
-rw-r--r--app/models/concerns/storage/legacy_namespace.rb51
-rw-r--r--app/models/environment.rb7
-rw-r--r--app/models/member.rb2
-rw-r--r--app/models/merge_request.rb5
-rw-r--r--app/models/project.rb49
-rw-r--r--app/models/project_auto_devops.rb11
-rw-r--r--app/models/project_services/jira_service.rb5
-rw-r--r--app/models/project_services/kubernetes_service.rb24
-rw-r--r--app/models/project_services/pipelines_email_service.rb4
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/service.rb5
-rw-r--r--app/services/files/create_service.rb8
-rw-r--r--app/services/files/multi_service.rb26
-rw-r--r--app/services/lfs/file_modification_handler.rb42
-rw-r--r--app/services/lfs/file_transformer.rb66
-rw-r--r--app/services/merge_requests/merge_request_diff_cache_service.rb11
-rw-r--r--app/services/notification_service.rb4
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/views/admin/runners/show.html.haml4
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml31
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/peek/views/_gc.html.haml7
-rw-r--r--app/views/peek/views/_gitaly.html.haml18
-rw-r--r--app/views/peek/views/_redis.html.haml7
-rw-r--r--app/views/peek/views/_sidekiq.html.haml7
-rw-r--r--app/views/peek/views/_sql.html.haml13
-rw-r--r--app/views/projects/services/_form.html.haml5
-rw-r--r--app/workers/project_export_worker.rb5
80 files changed, 2825 insertions, 2188 deletions
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 43a5325cf71..8259133c95b 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -132,9 +132,8 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
matcher(flag, subtext) {
- const relevantText = subtext.trim().split(/\s/).pop();
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
- const match = regexp.exec(relevantText);
+ const match = regexp.exec(subtext);
return match && match.length ? match[1] : null;
},
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index f01aef45500..e77318fef46 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -73,6 +73,7 @@ export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
const mergeRequestTabs = document.querySelector('.js-tabs-affix');
const navbar = document.querySelector('.navbar-gitlab');
+ const peek = document.getElementById('peek');
const paddingTop = 16;
this.diffsLoaded = false;
@@ -86,6 +87,10 @@ export default class MergeRequestTabs {
this.showTab = this.showTab.bind(this);
this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0;
+ if (peek) {
+ this.stickyTop += peek.offsetHeight;
+ }
+
if (mergeRequestTabs) {
this.stickyTop += mergeRequestTabs.offsetHeight;
}
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index 9e67a6f2146..42615d2bb8e 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -209,6 +209,7 @@
const xAxis = d3.axisBottom()
.scale(axisXScale)
+ .ticks(this.graphWidth / 120)
.tickFormat(timeScaleFormat);
const yAxis = d3.axisLeft()
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 972fdb2b791..096c4ef5f31 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -4,13 +4,15 @@ import discussionCounter from '../notes/components/discussion_counter.vue';
import store from '../notes/stores';
export default function initMrNotes() {
- new Vue({ // eslint-disable-line
+ // eslint-disable-next-line no-new
+ new Vue({
el: '#js-vue-mr-discussions',
components: {
notesApp,
},
data() {
- const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
+ const notesDataset = document.getElementById('js-vue-mr-discussions')
+ .dataset;
return {
noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData),
@@ -28,7 +30,8 @@ export default function initMrNotes() {
},
});
- new Vue({ // eslint-disable-line
+ // eslint-disable-next-line no-new
+ new Vue({
el: '#js-vue-discussion-counter',
components: {
discussionCounter,
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index c640003d958..659ae575219 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -16,6 +16,10 @@ import Autosize from 'autosize';
import 'vendor/jquery.caret'; // required by jquery.atwho
import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
+import Vue from 'vue';
+import syntaxHighlight from '~/syntax_highlight';
+import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { getLocationHash } from './lib/utils/url_utility';
import Flash from './flash';
@@ -24,7 +28,13 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import Autosave from './autosave';
import TaskList from './task_list';
-import { isInViewport, getPagePath, scrollToElement, isMetaKey, hasVueMRDiscussionsCookie } from './lib/utils/common_utils';
+import {
+ isInViewport,
+ getPagePath,
+ scrollToElement,
+ isMetaKey,
+ hasVueMRDiscussionsCookie,
+} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
@@ -38,9 +48,21 @@ const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
export default class Notes {
- static initialize(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
+ static initialize(
+ notes_url,
+ note_ids,
+ last_fetched_at,
+ view,
+ enableGFM = true,
+ ) {
if (!this.instance) {
- this.instance = new Notes(notes_url, note_ids, last_fetched_at, view, enableGFM);
+ this.instance = new Notes(
+ notes_url,
+ note_ids,
+ last_fetched_at,
+ view,
+ enableGFM,
+ );
}
}
@@ -78,7 +100,8 @@ export default class Notes {
this.updatedNotesTrackingMap = {};
this.last_fetched_at = last_fetched_at;
this.noteable_url = document.URL;
- this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
+ this.notesCountBadge ||
+ (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge'));
this.basePollingInterval = 15000;
this.maxPollingSteps = 4;
@@ -89,15 +112,24 @@ export default class Notes {
this.taskList = new TaskList({
dataType: 'note',
fieldName: 'note',
- selector: '.notes'
+ selector: '.notes',
});
this.collapseLongCommitList();
this.setViewType(view);
// We are in the Merge Requests page so we need another edit form for Changes tab
if (getPagePath(1) === 'merge_requests') {
- $('.note-edit-form').clone()
- .addClass('mr-note-edit-form').insertAfter('.note-edit-form');
+ $('.note-edit-form')
+ .clone()
+ .addClass('mr-note-edit-form')
+ .insertAfter('.note-edit-form');
+ }
+
+ const hash = getLocationHash();
+ const $anchor = hash && document.getElementById(hash);
+
+ if ($anchor) {
+ this.loadLazyDiff({ currentTarget: $anchor });
}
}
@@ -106,7 +138,9 @@ export default class Notes {
}
addBinding() {
- this.$wrapperEl = hasVueMRDiscussionsCookie() ? $(document).find('.diffs') : $(document);
+ this.$wrapperEl = hasVueMRDiscussionsCookie()
+ ? $(document).find('.diffs')
+ : $(document);
// Edit note link
this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
@@ -114,36 +148,78 @@ export default class Notes {
// Reopen and close actions for Issue/MR combined with note form submit
this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
- this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
+ this.$wrapperEl.on(
+ 'keyup input',
+ '.js-note-text',
+ this.updateTargetButtons,
+ );
// resolve a discussion
this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
+ this.$wrapperEl.on(
+ 'click',
+ '.js-note-attachment-delete',
+ this.removeAttachment,
+ );
// reset main target form when clicking discard
this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
+ this.$wrapperEl.on(
+ 'change',
+ '.js-note-attachment-input',
+ this.updateFormAttachment,
+ );
// reply to diff/discussion notes
- this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
+ this.$wrapperEl.on(
+ 'click',
+ '.js-discussion-reply-button',
+ this.onReplyToDiscussionNote,
+ );
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
- this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
+ this.$wrapperEl.on(
+ 'click',
+ '.js-add-image-diff-note-button',
+ this.onAddImageDiffNote,
+ );
// hide diff note form
- this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
+ this.$wrapperEl.on(
+ 'click',
+ '.js-close-discussion-note-form',
+ this.cancelDiscussionForm,
+ );
// toggle commit list
- this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
+ this.$wrapperEl.on(
+ 'click',
+ '.system-note-commit-list-toggler',
+ this.toggleCommitList,
+ );
+
+ this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff);
// fetch notes when tab becomes visible
this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
- this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
- this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
- this.$wrapperEl.on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
+ this.$wrapperEl.on(
+ 'ajax:success',
+ '.js-discussion-note-form',
+ this.addDiscussionNote,
+ );
+ this.$wrapperEl.on(
+ 'ajax:success',
+ '.js-main-target-form',
+ this.resetMainTargetForm,
+ );
+ this.$wrapperEl.on(
+ 'ajax:complete',
+ '.js-main-target-form',
+ this.reenableTargetFormSubmitButton,
+ );
// when a key is clicked on the notes
this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
@@ -173,6 +249,7 @@ export default class Notes {
this.$wrapperEl.off('keydown', '.js-note-text');
this.$wrapperEl.off('click', '.js-comment-resolve-button');
this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
+ this.$wrapperEl.off('click', '.js-toggle-lazy-diff');
this.$wrapperEl.off('ajax:success', '.js-main-target-form');
this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
@@ -181,10 +258,16 @@ export default class Notes {
}
static initCommentTypeToggle(form) {
- const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle');
- const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu');
+ const dropdownTrigger = form.querySelector(
+ '.js-comment-type-dropdown .dropdown-toggle',
+ );
+ const dropdownList = form.querySelector(
+ '.js-comment-type-dropdown .dropdown-menu',
+ );
const noteTypeInput = form.querySelector('#note_type');
- const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button');
+ const submitButton = form.querySelector(
+ '.js-comment-type-dropdown .js-comment-submit-button',
+ );
const closeButton = form.querySelector('.js-note-target-close');
const reopenButton = form.querySelector('.js-note-target-reopen');
@@ -201,7 +284,13 @@ export default class Notes {
}
keydownNoteText(e) {
- var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText;
+ var $textarea,
+ discussionNoteForm,
+ editNote,
+ myLastNote,
+ myLastNoteEditBtn,
+ newText,
+ originalText;
if (isMetaKey(e)) {
return;
}
@@ -213,7 +302,12 @@ export default class Notes {
if ($textarea.val() !== '') {
return;
}
- myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes'));
+ myLastNote = $(
+ `li.note[data-author-id='${
+ gon.current_user_id
+ }'][data-editable]:last`,
+ $textarea.closest('.note, .notes_holder, #notes'),
+ );
if (myLastNote.length) {
myLastNoteEditBtn = myLastNote.find('.js-note-edit');
return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
@@ -224,7 +318,9 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
- if (!confirm('Are you sure you want to cancel creating this comment?')) {
+ if (
+ !confirm('Are you sure you want to cancel creating this comment?')
+ ) {
return;
}
}
@@ -236,7 +332,9 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val();
if (originalText !== newText) {
- if (!confirm('Are you sure you want to cancel editing this comment?')) {
+ if (
+ !confirm('Are you sure you want to cancel editing this comment?')
+ ) {
return;
}
}
@@ -249,11 +347,14 @@ export default class Notes {
if (Notes.interval) {
clearInterval(Notes.interval);
}
- return Notes.interval = setInterval((function(_this) {
- return function() {
- return _this.refresh();
- };
- })(this), this.pollingInterval);
+ return (Notes.interval = setInterval(
+ (function(_this) {
+ return function() {
+ return _this.refresh();
+ };
+ })(this),
+ this.pollingInterval,
+ ));
}
refresh() {
@@ -269,20 +370,23 @@ export default class Notes {
this.refreshing = true;
- axios.get(`${this.notes_url}?html=true`, {
- headers: {
- 'X-Last-Fetched-At': this.last_fetched_at,
- },
- }).then(({ data }) => {
- const notes = data.notes;
- this.last_fetched_at = data.last_fetched_at;
- this.setPollingInterval(data.notes.length);
- $.each(notes, (i, note) => this.renderNote(note));
-
- this.refreshing = false;
- }).catch(() => {
- this.refreshing = false;
- });
+ axios
+ .get(`${this.notes_url}?html=true`, {
+ headers: {
+ 'X-Last-Fetched-At': this.last_fetched_at,
+ },
+ })
+ .then(({ data }) => {
+ const notes = data.notes;
+ this.last_fetched_at = data.last_fetched_at;
+ this.setPollingInterval(data.notes.length);
+ $.each(notes, (i, note) => this.renderNote(note));
+
+ this.refreshing = false;
+ })
+ .catch(() => {
+ this.refreshing = false;
+ });
}
/**
@@ -298,7 +402,8 @@ export default class Notes {
if (shouldReset == null) {
shouldReset = true;
}
- nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
+ nthInterval =
+ this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
if (shouldReset) {
this.pollingInterval = this.basePollingInterval;
} else if (this.pollingInterval < nthInterval) {
@@ -317,12 +422,17 @@ export default class Notes {
if ('emoji_award' in noteEntity.commands_changes) {
votesBlock = $('.js-awards-block').eq(0);
- loadAwardsHandler().then((awardsHandler) => {
- awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award);
- awardsHandler.scrollToAwards();
- }).catch(() => {
- // ignore
- });
+ loadAwardsHandler()
+ .then(awardsHandler => {
+ awardsHandler.addAwardToEmojiBar(
+ votesBlock,
+ noteEntity.commands_changes.emoji_award,
+ );
+ awardsHandler.scrollToAwards();
+ })
+ .catch(() => {
+ // ignore
+ });
}
}
}
@@ -367,11 +477,17 @@ export default class Notes {
if (!noteEntity.valid) {
if (noteEntity.errors && noteEntity.errors.commands_only) {
- if (noteEntity.commands_changes &&
- Object.keys(noteEntity.commands_changes).length > 0) {
+ if (
+ noteEntity.commands_changes &&
+ Object.keys(noteEntity.commands_changes).length > 0
+ ) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline.get(0));
+ this.addFlash(
+ noteEntity.errors.commands_only,
+ 'notice',
+ this.parentTimeline.get(0),
+ );
this.refresh();
}
return;
@@ -393,28 +509,30 @@ export default class Notes {
this.setupNewNote($newNote);
this.refresh();
return this.updateNotesCount(1);
- }
- // The server can send the same update multiple times so we need to make sure to only update once per actual update.
- else if (Notes.isUpdatedNote(noteEntity, $note)) {
+ } else if (Notes.isUpdatedNote(noteEntity, $note)) {
+ // The server can send the same update multiple times so we need to make sure to only update once per actual update.
const isEditing = $note.hasClass('is-editing');
const initialContent = normalizeNewlines(
- $note.find('.original-note-content').text().trim()
+ $note
+ .find('.original-note-content')
+ .text()
+ .trim(),
);
const $textarea = $note.find('.js-note-text');
const currentContent = $textarea.val();
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteNote = normalizeNewlines(noteEntity.note);
- const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote;
+ const isTextareaUntouched =
+ currentContent === initialContent ||
+ currentContent === sanitizedNoteNote;
if (isEditing && isTextareaUntouched) {
$textarea.val(noteEntity.note);
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
- }
- else if (isEditing && !isTextareaUntouched) {
+ } else if (isEditing && !isTextareaUntouched) {
this.putConflictEditWarningInPlace(noteEntity, $note);
this.updatedNotesTrackingMap[noteEntity.id] = noteEntity;
- }
- else {
+ } else {
const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note);
this.setupNewNote($updatedNote);
}
@@ -438,17 +556,31 @@ export default class Notes {
}
this.note_ids.push(noteEntity.id);
- form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`);
- row = (form.length || !noteEntity.discussion_line_code) ? form.closest('tr') : $(`#${noteEntity.discussion_line_code}`);
+ form =
+ $form ||
+ $(
+ `.js-discussion-note-form[data-discussion-id="${
+ noteEntity.discussion_id
+ }"]`,
+ );
+ row =
+ form.length || !noteEntity.discussion_line_code
+ ? form.closest('tr')
+ : $(`#${noteEntity.discussion_line_code}`);
if (noteEntity.on_image) {
row = form;
}
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
- diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
+ diffAvatarContainer = row
+ .prevAll('.line_holder')
+ .first()
+ .find('.js-avatar-container.' + lineType + '_line');
// is this the first note of discussion?
- discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
+ discussionContainer = $(
+ `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
+ );
if (!discussionContainer.length) {
discussionContainer = form.closest('.discussion').find('.notes');
}
@@ -456,25 +588,42 @@ export default class Notes {
if (noteEntity.diff_discussion_html) {
var $discussion = $(noteEntity.diff_discussion_html).renderGFM();
- if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
+ if (
+ !this.isParallelView() ||
+ row.hasClass('js-temp-notes-holder') ||
+ noteEntity.on_image
+ ) {
// insert the note and the reply button after the temp row
row.after($discussion);
} else {
// Merge new discussion HTML in
- var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
- var contentContainerClass = '.' + $notes.closest('.notes_content')
- .attr('class')
- .split(' ')
- .join('.');
-
- row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
+ var $notes = $discussion.find(
+ `.notes[data-discussion-id="${noteEntity.discussion_id}"]`,
+ );
+ var contentContainerClass =
+ '.' +
+ $notes
+ .closest('.notes_content')
+ .attr('class')
+ .split(' ')
+ .join('.');
+
+ row
+ .find(contentContainerClass + ' .content')
+ .append($notes.closest('.content').children());
}
}
// Init discussion on 'Discussion' page if it is merge request page
const page = $('body').attr('data-page');
- if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) {
+ if (
+ (page && page.indexOf('projects:merge_request') !== -1) ||
+ !noteEntity.diff_discussion_html
+ ) {
if (!hasVueMRDiscussionsCookie()) {
- Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
+ Notes.animateAppendNote(
+ noteEntity.discussion_html,
+ $('.main-notes-list'),
+ );
}
}
} else {
@@ -482,7 +631,10 @@ export default class Notes {
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
- if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
+ if (
+ typeof gl.diffNotesCompileComponents !== 'undefined' &&
+ noteEntity.discussion_resolvable
+ ) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
@@ -494,7 +646,8 @@ export default class Notes {
}
getLineHolder(changesDiscussionContainer) {
- return $(changesDiscussionContainer).closest('.notes_holder')
+ return $(changesDiscussionContainer)
+ .closest('.notes_holder')
.prevAll('.line_holder')
.first()
.get(0);
@@ -527,8 +680,14 @@ export default class Notes {
form.find('.js-errors').remove();
// reset text and preview
form.find('.js-md-write-button').click();
- form.find('.js-note-text').val('').trigger('input');
- form.find('.js-note-text').data('autosave').reset();
+ form
+ .find('.js-note-text')
+ .val('')
+ .trigger('input');
+ form
+ .find('.js-note-text')
+ .data('autosave')
+ .reset();
var event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
@@ -564,7 +723,10 @@ export default class Notes {
form.find('#note_type').val('');
form.find('#note_project_id').remove();
form.find('#in_reply_to_discussion_id').remove();
- form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
+ form
+ .find('.js-comment-resolve-button')
+ .closest('comment-and-resolve-btn')
+ .remove();
this.parentTimeline = form.parents('.timeline');
if (form.length) {
@@ -618,11 +780,17 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
- return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline.get(0));
+ return this.addFlash(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ 'alert',
+ formParentTimeline.get(0),
+ );
}
updateNoteError($parentTimeline) {
- new Flash('Your comment could not be updated! Please check your network connection and try again.');
+ new Flash(
+ 'Your comment could not be updated! Please check your network connection and try again.',
+ );
}
/**
@@ -671,14 +839,16 @@ export default class Notes {
}
checkContentToAllowEditing($el) {
- var initialContent = $el.find('.original-note-content').text().trim();
+ var initialContent = $el
+ .find('.original-note-content')
+ .text()
+ .trim();
var currentContent = $el.find('.js-note-text').val();
var isAllowed = true;
if (currentContent === initialContent) {
this.removeNoteEditForm($el);
- }
- else {
+ } else {
var $buttons = $el.find('.note-form-actions');
var isWidgetVisible = isInViewport($el.get(0));
@@ -740,8 +910,7 @@ export default class Notes {
this.setupNewNote($newNote);
// Now that we have taken care of the update, clear it out
delete this.updatedNotesTrackingMap[noteId];
- }
- else {
+ } else {
$note.find('.js-finish-edit-warning').hide();
this.removeNoteEditForm($note);
}
@@ -774,7 +943,9 @@ export default class Notes {
form.removeClass('current-note-edit-form');
form.find('.js-finish-edit-warning').hide();
// Replace markdown textarea text with original note text.
- return form.find('.js-note-text').val(form.find('form.edit-note').data('originalNote'));
+ return form
+ .find('.js-note-text')
+ .val(form.find('form.edit-note').data('originalNote'));
}
/**
@@ -788,58 +959,67 @@ export default class Notes {
$note = $(e.currentTarget).closest('.note');
noteElId = $note.attr('id');
noteId = $note.attr('data-note-id');
- lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
+ lineHolder = $(e.currentTarget)
+ .closest('.notes[data-discussion-id]')
.closest('.notes_holder')
.prev('.line_holder');
- $(`.note[id="${noteElId}"]`).each((function(_this) {
- // A same note appears in the "Discussion" and in the "Changes" tab, we have
- // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
- // where $('#noteId') would return only one.
- return function(i, el) {
- var $note, $notes;
- $note = $(el);
- $notes = $note.closest('.discussion-notes');
- const discussionId = $('.notes', $notes).data('discussionId');
-
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- if (gl.diffNoteApps[noteElId]) {
- gl.diffNoteApps[noteElId].$destroy();
+ $(`.note[id="${noteElId}"]`).each(
+ (function(_this) {
+ // A same note appears in the "Discussion" and in the "Changes" tab, we have
+ // to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
+ // where $('#noteId') would return only one.
+ return function(i, el) {
+ var $note, $notes;
+ $note = $(el);
+ $notes = $note.closest('.discussion-notes');
+ const discussionId = $('.notes', $notes).data('discussionId');
+
+ if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ if (gl.diffNoteApps[noteElId]) {
+ gl.diffNoteApps[noteElId].$destroy();
+ }
}
- }
-
- $note.remove();
-
- // check if this is the last note for this line
- if ($notes.find('.note').length === 0) {
- var notesTr = $notes.closest('tr');
-
- // "Discussions" tab
- $notes.closest('.timeline-entry').remove();
-
- $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
-
- // The notes tr can contain multiple lists of notes, like on the parallel diff
- // notesTr does not exist for image diffs
- if (notesTr.find('.discussion-notes').length > 1 || notesTr.length === 0) {
- const $diffFile = $notes.closest('.diff-file');
- if ($diffFile.length > 0) {
- const removeBadgeEvent = new CustomEvent('removeBadge.imageDiff', {
- detail: {
- // badgeNumber's start with 1 and index starts with 0
- badgeNumber: $notes.index() + 1,
- },
- });
- $diffFile[0].dispatchEvent(removeBadgeEvent);
+ $note.remove();
+
+ // check if this is the last note for this line
+ if ($notes.find('.note').length === 0) {
+ var notesTr = $notes.closest('tr');
+
+ // "Discussions" tab
+ $notes.closest('.timeline-entry').remove();
+
+ $(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
+
+ // The notes tr can contain multiple lists of notes, like on the parallel diff
+ // notesTr does not exist for image diffs
+ if (
+ notesTr.find('.discussion-notes').length > 1 ||
+ notesTr.length === 0
+ ) {
+ const $diffFile = $notes.closest('.diff-file');
+ if ($diffFile.length > 0) {
+ const removeBadgeEvent = new CustomEvent(
+ 'removeBadge.imageDiff',
+ {
+ detail: {
+ // badgeNumber's start with 1 and index starts with 0
+ badgeNumber: $notes.index() + 1,
+ },
+ },
+ );
+
+ $diffFile[0].dispatchEvent(removeBadgeEvent);
+ }
+
+ $notes.remove();
+ } else if (notesTr.length > 0) {
+ notesTr.remove();
}
-
- $notes.remove();
- } else if (notesTr.length > 0) {
- notesTr.remove();
}
- }
- };
- })(this));
+ };
+ })(this),
+ );
Notes.refreshVueNotes();
Notes.checkMergeRequestStatus();
@@ -921,7 +1101,12 @@ export default class Notes {
// DiffNote
form.find('#note_position').val(dataHolder.attr('data-position'));
- form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancelText'));
+ form
+ .find('.js-note-discard')
+ .show()
+ .removeClass('js-note-discard')
+ .addClass('js-close-discussion-note-form')
+ .text(form.find('.js-close-discussion-note-form').data('cancelText'));
form.find('.js-note-target-close').remove();
form.find('.js-note-new-discussion').remove();
this.setupNoteForm(form);
@@ -957,7 +1142,7 @@ export default class Notes {
this.toggleDiffNote({
target: $link,
lineType: link.dataset.lineType,
- showReplyInput
+ showReplyInput,
});
}
@@ -973,7 +1158,9 @@ export default class Notes {
// Setup comment form
let newForm;
- const $noteContainer = $link.closest('.diff-viewer').find('.note-container');
+ const $noteContainer = $link
+ .closest('.diff-viewer')
+ .find('.note-container');
const $form = $noteContainer.find('> .discussion-form');
if ($form.length === 0) {
@@ -986,13 +1173,17 @@ export default class Notes {
this.setupDiscussionNoteForm($link, newForm);
}
- toggleDiffNote({
- target,
- lineType,
- forceShow,
- showReplyInput = false,
- }) {
- var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
+ toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) {
+ var $link,
+ addForm,
+ hasNotes,
+ newForm,
+ noteForm,
+ replyButton,
+ row,
+ rowCssToAdd,
+ targetContent,
+ isDiffCommentAvatar;
$link = $(target);
row = $link.closest('tr');
const nextRow = row.next();
@@ -1004,11 +1195,13 @@ export default class Notes {
hasNotes = nextRow.is('.notes_holder');
addForm = false;
let lineTypeSelector = '';
- rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
+ rowCssToAdd =
+ '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>';
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineTypeSelector = `.${lineType}`;
- rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
+ rowCssToAdd =
+ '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>';
}
const notesContentSelector = `.notes_content${lineTypeSelector} .content`;
let notesContent = targetRow.find(notesContentSelector);
@@ -1036,7 +1229,9 @@ export default class Notes {
notesContent = targetRow.find(notesContentSelector);
addForm = true;
} else {
- const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible');
+ const isCurrentlyShown = targetRow
+ .find('.content:not(:empty)')
+ .is(':visible');
const isForced = forceShow === true || forceShow === false;
const showNow = forceShow === true || (!isCurrentlyShown && !isForced);
@@ -1063,11 +1258,12 @@ export default class Notes {
row = form.closest('tr');
glForm = form.data('glForm');
glForm.destroy();
- form.find('.js-note-text').data('autosave').reset();
- // show the reply button (will only work for replies)
form
- .prev('.discussion-reply-holder')
- .show();
+ .find('.js-note-text')
+ .data('autosave')
+ .reset();
+ // show the reply button (will only work for replies)
+ form.prev('.discussion-reply-holder').show();
if (row.is('.js-temp-notes-holder')) {
// remove temporary row for diff lines
return row.remove();
@@ -1108,7 +1304,9 @@ export default class Notes {
var filename, form;
form = $(this).closest('form');
// get only the basename
- filename = $(this).val().replace(/^.*[\\\/]/, '');
+ filename = $(this)
+ .val()
+ .replace(/^.*[\\\/]/, '');
return form.find('.js-attachment-filename').text(filename);
}
@@ -1180,12 +1378,16 @@ export default class Notes {
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
- $editForm.find('form')
+ $editForm
+ .find('form')
.attr('action', `${postUrl}?html=true`)
.attr('data-remote', 'true');
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
- $editForm.find('.js-note-text').focus().val(originalContent);
+ $editForm
+ .find('.js-note-text')
+ .focus()
+ .val(originalContent);
$editForm.find('.js-md-write-button').trigger('click');
$editForm.find('.referenced-users').hide();
}
@@ -1194,7 +1396,9 @@ export default class Notes {
if ($note.find('.js-conflict-edit-warning').length === 0) {
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
This comment has changed since you started editing, please review the
- <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
+ <a href="#note_${
+ noteEntity.id
+ }" target="_blank" rel="noopener noreferrer">
updated comment
</a>
to ensure information is not lost
@@ -1204,14 +1408,79 @@ export default class Notes {
}
updateNotesCount(updateCount) {
- return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
+ return this.notesCountBadge.text(
+ parseInt(this.notesCountBadge.text(), 10) + updateCount,
+ );
+ }
+
+ static renderPlaceholderComponent($container) {
+ const el = $container.find('.js-code-placeholder').get(0);
+ new Vue({
+ // eslint-disable-line no-new
+ el,
+ components: {
+ SkeletonLoadingContainer,
+ },
+ render(createElement) {
+ return createElement('skeleton-loading-container');
+ },
+ });
+ }
+
+ static renderDiffContent($container, data) {
+ const { discussion_html } = data;
+ const lines = $(discussion_html).find('.line_holder');
+ lines.addClass('fade-in');
+ $container.find('tbody').prepend(lines);
+ const fileHolder = $container.find('.file-holder');
+ $container.find('.line-holder-placeholder').remove();
+ syntaxHighlight(fileHolder);
+ }
+
+ static renderDiffError($container) {
+ $container.find('.line_content').html(
+ $(`
+ <div class="nothing-here-block">
+ ${__(
+ 'Unable to load the diff.',
+ )} <a class="js-toggle-lazy-diff" href="javascript:void(0)">Try again</a>?
+ </div>
+ `),
+ );
+ }
+
+ loadLazyDiff(e) {
+ const $container = $(e.currentTarget).closest('.js-toggle-container');
+ Notes.renderPlaceholderComponent($container);
+
+ $container.find('.js-toggle-lazy-diff').removeClass('js-toggle-lazy-diff');
+
+ const tableEl = $container.find('tbody');
+ if (tableEl.length === 0) return;
+
+ const fileHolder = $container.find('.file-holder');
+ const url = fileHolder.data('linesPath');
+
+ axios
+ .get(url)
+ .then(({ data }) => {
+ Notes.renderDiffContent($container, data);
+ })
+ .catch(() => {
+ Notes.renderDiffError($container);
+ });
}
toggleCommitList(e) {
const $element = $(e.currentTarget);
- const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
+ const $closestSystemCommitList = $element.siblings(
+ '.system-note-commit-list',
+ );
- $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
+ $element
+ .find('.fa')
+ .toggleClass('fa-angle-down')
+ .toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade');
}
@@ -1221,11 +1490,17 @@ export default class Notes {
* intrusive.
*/
collapseLongCommitList() {
- const systemNotes = $('#notes-list').find('li.system-note').has('ul');
+ const systemNotes = $('#notes-list')
+ .find('li.system-note')
+ .has('ul');
$.each(systemNotes, function(index, systemNote) {
const $systemNote = $(systemNote);
- const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', '');
+ const headerMessage = $systemNote
+ .find('.note-text')
+ .find('p:first')
+ .text()
+ .replace(':', '');
$systemNote.find('.note-header .system-note-message').html(headerMessage);
@@ -1233,7 +1508,9 @@ export default class Notes {
$systemNote.find('.note-text').addClass('system-note-commit-list');
$systemNote.find('.system-note-commit-list-toggler').show();
} else {
- $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade');
+ $systemNote
+ .find('.note-text')
+ .addClass('system-note-commit-list hide-shade');
}
});
}
@@ -1251,14 +1528,10 @@ export default class Notes {
cleanForm($form) {
// Remove JS classes that are not needed here
- $form
- .find('.js-comment-type-dropdown')
- .removeClass('btn-group');
+ $form.find('.js-comment-type-dropdown').removeClass('btn-group');
// Remove dropdown
- $form
- .find('.dropdown-menu')
- .remove();
+ $form.find('.dropdown-menu').remove();
return $form;
}
@@ -1277,7 +1550,11 @@ export default class Notes {
// There can be CRLF vs LF mismatches if we don't sanitize and compare the same way
const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim());
const currentNoteText = normalizeNewlines(
- $note.find('.original-note-content').first().text().trim()
+ $note
+ .find('.original-note-content')
+ .first()
+ .text()
+ .trim(),
);
return sanitizedNoteEntityText !== currentNoteText;
}
@@ -1367,7 +1644,14 @@ export default class Notes {
* Once comment is _actually_ posted on server, we will have final element
* in response that we will show in place of this temporary element.
*/
- createPlaceholderNote({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) {
+ createPlaceholderNote({
+ formContent,
+ uniqueId,
+ isDiscussionNote,
+ currentUsername,
+ currentUserFullname,
+ currentUserAvatar,
+ }) {
const discussionClass = isDiscussionNote ? 'discussion' : '';
const $tempNote = $(
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
@@ -1381,8 +1665,12 @@ export default class Notes {
<div class="note-header">
<div class="note-header-info">
<a href="/${_.escape(currentUsername)}">
- <span class="hidden-xs">${_.escape(currentUsername)}</span>
- <span class="note-headline-light">${_.escape(currentUsername)}</span>
+ <span class="hidden-xs">${_.escape(
+ currentUsername,
+ )}</span>
+ <span class="note-headline-light">${_.escape(
+ currentUsername,
+ )}</span>
</a>
</div>
</div>
@@ -1393,11 +1681,13 @@ export default class Notes {
</div>
</div>
</div>
- </li>`
+ </li>`,
);
$tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
- $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
+ $tempNote
+ .find('.note-headline-light')
+ .text(`@${_.escape(currentUsername)}`);
return $tempNote;
}
@@ -1413,7 +1703,7 @@ export default class Notes {
<i>${formContent}</i>
</div>
</div>
- </li>`
+ </li>`,
);
return $tempNote;
@@ -1445,11 +1735,22 @@ export default class Notes {
const $submitBtn = $(e.target);
let $form = $submitBtn.parents('form');
const $closeBtn = $form.find('.js-note-target-close');
- const isDiscussionNote = $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion';
+ const isDiscussionNote =
+ $submitBtn
+ .parent()
+ .find('li.droplab-item-selected')
+ .attr('id') === 'discussion';
const isMainForm = $form.hasClass('js-main-target-form');
const isDiscussionForm = $form.hasClass('js-discussion-note-form');
- const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button');
- const { formData, formContent, formAction, formContentOriginal } = this.getFormData($form);
+ const isDiscussionResolve = $submitBtn.hasClass(
+ 'js-comment-resolve-button',
+ );
+ const {
+ formData,
+ formContent,
+ formAction,
+ formContentOriginal,
+ } = this.getFormData($form);
let noteUniqueId;
let systemNoteUniqueId;
let hasQuickActions = false;
@@ -1479,23 +1780,30 @@ export default class Notes {
// Show placeholder note
if (tempFormContent) {
noteUniqueId = _.uniqueId('tempNote_');
- $notesContainer.append(this.createPlaceholderNote({
- formContent: tempFormContent,
- uniqueId: noteUniqueId,
- isDiscussionNote,
- currentUsername: gon.current_username,
- currentUserFullname: gon.current_user_fullname,
- currentUserAvatar: gon.current_user_avatar_url,
- }));
+ $notesContainer.append(
+ this.createPlaceholderNote({
+ formContent: tempFormContent,
+ uniqueId: noteUniqueId,
+ isDiscussionNote,
+ currentUsername: gon.current_username,
+ currentUserFullname: gon.current_user_fullname,
+ currentUserAvatar: gon.current_user_avatar_url,
+ }),
+ );
}
// Show placeholder system note
if (hasQuickActions) {
systemNoteUniqueId = _.uniqueId('tempSystemNote_');
- $notesContainer.append(this.createPlaceholderSystemNote({
- formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)),
- uniqueId: systemNoteUniqueId,
- }));
+ $notesContainer.append(
+ this.createPlaceholderSystemNote({
+ formContent: this.getQuickActionDescription(
+ formContent,
+ AjaxCache.get(gl.GfmAutoComplete.dataSources.commands),
+ ),
+ uniqueId: systemNoteUniqueId,
+ }),
+ );
}
// Clear the form textarea
@@ -1509,8 +1817,9 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
- axios.post(`${formAction}?html=true`, formData)
- .then((res) => {
+ axios
+ .post(`${formAction}?html=true`, formData)
+ .then(res => {
const note = res.data;
// Submission successful! remove placeholder
@@ -1527,7 +1836,9 @@ export default class Notes {
// Reset cached commands list when command is applied
if (hasQuickActions) {
- $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho');
+ $form
+ .find('textarea.js-note-text')
+ .trigger('clear-commands-cache.atwho');
}
// Clear previous form errors
@@ -1572,11 +1883,14 @@ export default class Notes {
// append flash-container to the Notes list
if ($notesContainer.length) {
- $notesContainer.append('<div class="flash-container" style="display: none;"></div>');
+ $notesContainer.append(
+ '<div class="flash-container" style="display: none;"></div>',
+ );
}
Notes.refreshVueNotes();
- } else if (isMainForm) { // Check if this was main thread comment
+ } else if (isMainForm) {
+ // Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
this.addNote($form, note);
this.reenableTargetFormSubmitButton(e);
@@ -1587,7 +1901,8 @@ export default class Notes {
}
$form.trigger('ajax:success', [note]);
- }).catch(() => {
+ })
+ .catch(() => {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
@@ -1607,7 +1922,9 @@ export default class Notes {
// Show form again on UI on failure
if (isDiscussionForm && $notesContainer.length) {
- const replyButton = $notesContainer.parent().find('.js-discussion-reply-button');
+ const replyButton = $notesContainer
+ .parent()
+ .find('.js-discussion-reply-button');
this.replyToDiscussionNote(replyButton[0]);
$form = $notesContainer.parent().find('form');
}
@@ -1652,12 +1969,19 @@ export default class Notes {
// Show updated comment content temporarily
$noteBodyText.html(formContent);
- $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half');
- $editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>');
+ $editingNote
+ .removeClass('is-editing fade-in-full')
+ .addClass('being-posted fade-in-half');
+ $editingNote
+ .find('.note-headline-meta a')
+ .html(
+ '<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>',
+ );
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
- axios.post(`${formAction}?html=true`, formData)
+ axios
+ .post(`${formAction}?html=true`, formData)
.then(({ data }) => {
// Submission successful! render final note element
this.updateNote(data, $editingNote);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 1785be01a0d..90dcafd75b7 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -1,298 +1,311 @@
<script>
- import $ from 'jquery';
- import { mapActions, mapGetters } from 'vuex';
- import _ from 'underscore';
- import Autosize from 'autosize';
- import { __, sprintf } from '~/locale';
- import Flash from '../../flash';
- import Autosave from '../../autosave';
- import TaskList from '../../task_list';
- import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility';
- import * as constants from '../constants';
- import eventHub from '../event_hub';
- import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
- import markdownField from '../../vue_shared/components/markdown/field.vue';
- import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import loadingButton from '../../vue_shared/components/loading_button.vue';
- import noteSignedOutWidget from './note_signed_out_widget.vue';
- import discussionLockedWidget from './discussion_locked_widget.vue';
- import issuableStateMixin from '../mixins/issuable_state';
+import $ from 'jquery';
+import { mapActions, mapGetters, mapState } from 'vuex';
+import _ from 'underscore';
+import Autosize from 'autosize';
+import { __, sprintf } from '~/locale';
+import Flash from '../../flash';
+import Autosave from '../../autosave';
+import TaskList from '../../task_list';
+import {
+ capitalizeFirstCharacter,
+ convertToCamelCase,
+} from '../../lib/utils/text_utility';
+import * as constants from '../constants';
+import eventHub from '../event_hub';
+import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
+import markdownField from '../../vue_shared/components/markdown/field.vue';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import loadingButton from '../../vue_shared/components/loading_button.vue';
+import noteSignedOutWidget from './note_signed_out_widget.vue';
+import discussionLockedWidget from './discussion_locked_widget.vue';
+import issuableStateMixin from '../mixins/issuable_state';
- export default {
- name: 'CommentForm',
- components: {
- issueWarning,
- noteSignedOutWidget,
- discussionLockedWidget,
- markdownField,
- userAvatarLink,
- loadingButton,
+export default {
+ name: 'CommentForm',
+ components: {
+ issueWarning,
+ noteSignedOutWidget,
+ discussionLockedWidget,
+ markdownField,
+ userAvatarLink,
+ loadingButton,
+ },
+ mixins: [issuableStateMixin],
+ props: {
+ noteableType: {
+ type: String,
+ required: true,
},
- mixins: [
- issuableStateMixin,
- ],
- props: {
- noteableType: {
- type: String,
- required: true,
- },
+ },
+ data() {
+ return {
+ note: '',
+ noteType: constants.COMMENT,
+ isSubmitting: false,
+ isSubmitButtonDisabled: true,
+ };
+ },
+ computed: {
+ ...mapGetters([
+ 'getCurrentUserLastNote',
+ 'getUserData',
+ 'getNoteableData',
+ 'getNotesData',
+ 'openState',
+ ]),
+ ...mapState(['isToggleStateButtonLoading']),
+ noteableDisplayName() {
+ return this.noteableType.replace(/_/g, ' ');
},
- data() {
- return {
- note: '',
- noteType: constants.COMMENT,
- isSubmitting: false,
- isSubmitButtonDisabled: true,
- };
+ isLoggedIn() {
+ return this.getUserData.id;
+ },
+ commentButtonTitle() {
+ return this.noteType === constants.COMMENT
+ ? 'Comment'
+ : 'Start discussion';
+ },
+ isOpen() {
+ return (
+ this.openState === constants.OPENED ||
+ this.openState === constants.REOPENED
+ );
},
- computed: {
- ...mapGetters([
- 'getCurrentUserLastNote',
- 'getUserData',
- 'getNoteableData',
- 'getNotesData',
- 'openState',
- ]),
- noteableDisplayName() {
- return this.noteableType.replace(/_/g, ' ');
- },
- isLoggedIn() {
- return this.getUserData.id;
- },
- commentButtonTitle() {
- return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
- },
- isOpen() {
- return this.openState === constants.OPENED || this.openState === constants.REOPENED;
- },
- canCreateNote() {
- return this.getNoteableData.current_user.can_create_note;
- },
- issueActionButtonTitle() {
- const openOrClose = this.isOpen ? 'close' : 'reopen';
+ canCreateNote() {
+ return this.getNoteableData.current_user.can_create_note;
+ },
+ issueActionButtonTitle() {
+ const openOrClose = this.isOpen ? 'close' : 'reopen';
- if (this.note.length) {
- return sprintf(
- __('%{actionText} & %{openOrClose} %{noteable}'),
- {
- actionText: this.commentButtonTitle,
- openOrClose,
- noteable: this.noteableDisplayName,
- },
- );
- }
+ if (this.note.length) {
+ return sprintf(__('%{actionText} & %{openOrClose} %{noteable}'), {
+ actionText: this.commentButtonTitle,
+ openOrClose,
+ noteable: this.noteableDisplayName,
+ });
+ }
- return sprintf(
- __('%{openOrClose} %{noteable}'),
- {
- openOrClose: capitalizeFirstCharacter(openOrClose),
- noteable: this.noteableDisplayName,
- },
- );
- },
- actionButtonClassNames() {
- return {
- 'btn-reopen': !this.isOpen,
- 'btn-close': this.isOpen,
- 'js-note-target-close': this.isOpen,
- 'js-note-target-reopen': !this.isOpen,
- };
- },
- markdownDocsPath() {
- return this.getNotesData.markdownDocsPath;
- },
- quickActionsDocsPath() {
- return this.getNotesData.quickActionsDocsPath;
- },
- markdownPreviewPath() {
- return this.getNoteableData.preview_note_path;
- },
- author() {
- return this.getUserData;
- },
- canUpdateIssue() {
- return this.getNoteableData.current_user.can_update;
- },
- endpoint() {
- return this.getNoteableData.create_note_path;
- },
+ return sprintf(__('%{openOrClose} %{noteable}'), {
+ openOrClose: capitalizeFirstCharacter(openOrClose),
+ noteable: this.noteableDisplayName,
+ });
},
- watch: {
- note(newNote) {
- this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
- },
- isSubmitting(newValue) {
- this.setIsSubmitButtonDisabled(this.note, newValue);
- },
+ actionButtonClassNames() {
+ return {
+ 'btn-reopen': !this.isOpen,
+ 'btn-close': this.isOpen,
+ 'js-note-target-close': this.isOpen,
+ 'js-note-target-reopen': !this.isOpen,
+ };
},
- mounted() {
- // jQuery is needed here because it is a custom event being dispatched with jQuery.
- $(document).on('issuable:change', (e, isClosed) => {
- this.toggleIssueLocalState(isClosed ? constants.CLOSED : constants.REOPENED);
- });
+ markdownDocsPath() {
+ return this.getNotesData.markdownDocsPath;
+ },
+ quickActionsDocsPath() {
+ return this.getNotesData.quickActionsDocsPath;
+ },
+ markdownPreviewPath() {
+ return this.getNoteableData.preview_note_path;
+ },
+ author() {
+ return this.getUserData;
+ },
+ canUpdateIssue() {
+ return this.getNoteableData.current_user.can_update;
+ },
+ endpoint() {
+ return this.getNoteableData.create_note_path;
+ },
+ },
+ watch: {
+ note(newNote) {
+ this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
+ },
+ isSubmitting(newValue) {
+ this.setIsSubmitButtonDisabled(this.note, newValue);
+ },
+ },
+ mounted() {
+ // jQuery is needed here because it is a custom event being dispatched with jQuery.
+ $(document).on('issuable:change', (e, isClosed) => {
+ this.toggleIssueLocalState(
+ isClosed ? constants.CLOSED : constants.REOPENED,
+ );
+ });
- this.initAutoSave();
- this.initTaskList();
+ this.initAutoSave();
+ this.initTaskList();
+ },
+ methods: {
+ ...mapActions([
+ 'saveNote',
+ 'stopPolling',
+ 'restartPolling',
+ 'removePlaceholderNotes',
+ 'closeIssue',
+ 'reopenIssue',
+ 'toggleIssueLocalState',
+ 'toggleStateButtonLoading',
+ ]),
+ setIsSubmitButtonDisabled(note, isSubmitting) {
+ if (!_.isEmpty(note) && !isSubmitting) {
+ this.isSubmitButtonDisabled = false;
+ } else {
+ this.isSubmitButtonDisabled = true;
+ }
},
- methods: {
- ...mapActions([
- 'saveNote',
- 'stopPolling',
- 'restartPolling',
- 'removePlaceholderNotes',
- 'closeIssue',
- 'reopenIssue',
- 'toggleIssueLocalState',
- ]),
- setIsSubmitButtonDisabled(note, isSubmitting) {
- if (!_.isEmpty(note) && !isSubmitting) {
- this.isSubmitButtonDisabled = false;
- } else {
- this.isSubmitButtonDisabled = true;
- }
- },
- handleSave(withIssueAction) {
- this.isSubmitting = true;
+ handleSave(withIssueAction) {
+ this.isSubmitting = true;
- if (this.note.length) {
- const noteData = {
- endpoint: this.endpoint,
- flashContainer: this.$el,
- data: {
- note: {
- noteable_type: this.noteableType,
- noteable_id: this.getNoteableData.id,
- note: this.note,
- },
+ if (this.note.length) {
+ const noteData = {
+ endpoint: this.endpoint,
+ flashContainer: this.$el,
+ data: {
+ note: {
+ noteable_type: this.noteableType,
+ noteable_id: this.getNoteableData.id,
+ note: this.note,
},
- };
+ },
+ };
- if (this.noteType === constants.DISCUSSION) {
- noteData.data.note.type = constants.DISCUSSION_NOTE;
- }
- this.note = ''; // Empty textarea while being requested. Repopulate in catch
- this.resizeTextarea();
- this.stopPolling();
+ if (this.noteType === constants.DISCUSSION) {
+ noteData.data.note.type = constants.DISCUSSION_NOTE;
+ }
- this.saveNote(noteData)
- .then((res) => {
- this.isSubmitting = false;
- this.restartPolling();
+ this.note = ''; // Empty textarea while being requested. Repopulate in catch
+ this.resizeTextarea();
+ this.stopPolling();
- if (res.errors) {
- if (res.errors.commands_only) {
- this.discard();
- } else {
- Flash(
- 'Something went wrong while adding your comment. Please try again.',
- 'alert',
- this.$refs.commentForm,
- );
- }
- } else {
+ this.saveNote(noteData)
+ .then(res => {
+ this.enableButton();
+ this.restartPolling();
+
+ if (res.errors) {
+ if (res.errors.commands_only) {
this.discard();
+ } else {
+ Flash(
+ 'Something went wrong while adding your comment. Please try again.',
+ 'alert',
+ this.$refs.commentForm,
+ );
}
+ } else {
+ this.discard();
+ }
- if (withIssueAction) {
- this.toggleIssueState();
- }
- })
- .catch(() => {
- this.isSubmitting = false;
- this.discard(false);
- const msg =
- `Your comment could not be submitted!
+ if (withIssueAction) {
+ this.toggleIssueState();
+ }
+ })
+ .catch(() => {
+ this.enableButton();
+ this.discard(false);
+ const msg = `Your comment could not be submitted!
Please check your network connection and try again.`;
- Flash(msg, 'alert', this.$el);
- this.note = noteData.data.note.note; // Restore textarea content.
- this.removePlaceholderNotes();
- });
- } else {
- this.toggleIssueState();
- }
- },
- enableButton() {
- this.isSubmitting = false;
- },
- toggleIssueState() {
- if (this.isOpen) {
- this.closeIssue()
- .then(() => this.enableButton())
- .catch(() => {
- this.enableButton();
- Flash(
- sprintf(
- __('Something went wrong while closing the %{issuable}. Please try again later'),
- { issuable: this.noteableDisplayName },
+ Flash(msg, 'alert', this.$el);
+ this.note = noteData.data.note.note; // Restore textarea content.
+ this.removePlaceholderNotes();
+ });
+ } else {
+ this.toggleIssueState();
+ }
+ },
+ enableButton() {
+ this.isSubmitting = false;
+ },
+ toggleIssueState() {
+ if (this.isOpen) {
+ this.closeIssue()
+ .then(() => this.enableButton())
+ .catch(() => {
+ this.enableButton();
+ this.toggleStateButtonLoading(false);
+ Flash(
+ sprintf(
+ __(
+ 'Something went wrong while closing the %{issuable}. Please try again later',
),
- );
- });
- } else {
- this.reopenIssue()
- .then(() => this.enableButton())
- .catch(() => {
- this.enableButton();
- Flash(
- sprintf(
- __('Something went wrong while reopening the %{issuable}. Please try again later'),
- { issuable: this.noteableDisplayName },
+ { issuable: this.noteableDisplayName },
+ ),
+ );
+ });
+ } else {
+ this.reopenIssue()
+ .then(() => this.enableButton())
+ .catch(() => {
+ this.enableButton();
+ this.toggleStateButtonLoading(false);
+ Flash(
+ sprintf(
+ __(
+ 'Something went wrong while reopening the %{issuable}. Please try again later',
),
- );
- });
- }
- },
- discard(shouldClear = true) {
- // `blur` is needed to clear slash commands autocomplete cache if event fired.
- // `focus` is needed to remain cursor in the textarea.
- this.$refs.textarea.blur();
- this.$refs.textarea.focus();
+ { issuable: this.noteableDisplayName },
+ ),
+ );
+ });
+ }
+ },
+ discard(shouldClear = true) {
+ // `blur` is needed to clear slash commands autocomplete cache if event fired.
+ // `focus` is needed to remain cursor in the textarea.
+ this.$refs.textarea.blur();
+ this.$refs.textarea.focus();
- if (shouldClear) {
- this.note = '';
- this.resizeTextarea();
- this.$refs.markdownField.previewMarkdown = false;
- }
+ if (shouldClear) {
+ this.note = '';
+ this.resizeTextarea();
+ this.$refs.markdownField.previewMarkdown = false;
+ }
- this.autosave.reset();
- },
- setNoteType(type) {
- this.noteType = type;
- },
- editCurrentUserLastNote() {
- if (this.note === '') {
- const lastNote = this.getCurrentUserLastNote;
+ this.autosave.reset();
+ },
+ setNoteType(type) {
+ this.noteType = type;
+ },
+ editCurrentUserLastNote() {
+ if (this.note === '') {
+ const lastNote = this.getCurrentUserLastNote;
- if (lastNote) {
- eventHub.$emit('enterEditMode', {
- noteId: lastNote.id,
- });
- }
+ if (lastNote) {
+ eventHub.$emit('enterEditMode', {
+ noteId: lastNote.id,
+ });
}
- },
- initAutoSave() {
- if (this.isLoggedIn) {
- const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
+ }
+ },
+ initAutoSave() {
+ if (this.isLoggedIn) {
+ const noteableType = capitalizeFirstCharacter(
+ convertToCamelCase(this.noteableType),
+ );
- this.autosave = new Autosave(
- $(this.$refs.textarea),
- ['Note', noteableType, this.getNoteableData.id],
- );
- }
- },
- initTaskList() {
- return new TaskList({
- dataType: 'note',
- fieldName: 'note',
- selector: '.notes',
- });
- },
- resizeTextarea() {
- this.$nextTick(() => {
- Autosize.update(this.$refs.textarea);
- });
- },
+ this.autosave = new Autosave($(this.$refs.textarea), [
+ 'Note',
+ noteableType,
+ this.getNoteableData.id,
+ ]);
+ }
+ },
+ initTaskList() {
+ return new TaskList({
+ dataType: 'note',
+ fieldName: 'note',
+ selector: '.notes',
+ });
},
- };
+ resizeTextarea() {
+ this.$nextTick(() => {
+ Autosize.update(this.$refs.textarea);
+ });
+ },
+ },
+};
</script>
<template>
@@ -419,13 +432,13 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<loading-button
v-if="canUpdateIssue"
- :loading="isSubmitting"
+ :loading="isToggleStateButtonLoading"
@click="handleSave(true)"
:container-class="[
actionButtonClassNames,
'btn btn-comment btn-comment-and-close js-action-button'
]"
- :disabled="isSubmitting"
+ :disabled="isToggleStateButtonLoading || isSubmitting"
:label="issueActionButtonTitle"
/>
diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue
index 3bcde17f07c..94d9dc69964 100644
--- a/app/assets/javascripts/notes/components/diff_file_header.vue
+++ b/app/assets/javascripts/notes/components/diff_file_header.vue
@@ -1,24 +1,24 @@
<script>
- import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
- import Icon from '~/vue_shared/components/icon.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import Icon from '~/vue_shared/components/icon.vue';
- export default {
- components: {
- ClipboardButton,
- Icon,
+export default {
+ components: {
+ ClipboardButton,
+ Icon,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
},
- props: {
- diffFile: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ titleTag() {
+ return this.diffFile.discussionPath ? 'a' : 'span';
},
- computed: {
- titleTag() {
- return this.diffFile.discussionPath ? 'a' : 'span';
- },
- },
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 1dba84fac18..ee01ec85bbb 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,56 +1,60 @@
<script>
- import $ from 'jquery';
- import syntaxHighlight from '~/syntax_highlight';
- import imageDiffHelper from '~/image_diff/helpers/index';
- import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
- import DiffFileHeader from './diff_file_header.vue';
+import $ from 'jquery';
+import syntaxHighlight from '~/syntax_highlight';
+import imageDiffHelper from '~/image_diff/helpers/index';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import DiffFileHeader from './diff_file_header.vue';
- export default {
- components: {
- DiffFileHeader,
+export default {
+ components: {
+ DiffFileHeader,
+ },
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
},
- props: {
- discussion: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ isImageDiff() {
+ return !this.diffFile.text;
},
- computed: {
- isImageDiff() {
- return !this.diffFile.text;
- },
- diffFileClass() {
- const { text } = this.diffFile;
- return text ? 'text-file' : 'js-image-file';
- },
- diffRows() {
- return $(this.discussion.truncatedDiffLines);
- },
- diffFile() {
- return convertObjectPropsToCamelCase(this.discussion.diffFile);
- },
- imageDiffHtml() {
- return this.discussion.imageDiffHtml;
- },
+ diffFileClass() {
+ const { text } = this.diffFile;
+ return text ? 'text-file' : 'js-image-file';
},
- mounted() {
- if (this.isImageDiff) {
- const canCreateNote = false;
- const renderCommentBadge = true;
- imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
- } else {
- const fileHolder = $(this.$refs.fileHolder);
- this.$nextTick(() => {
- syntaxHighlight(fileHolder);
- });
- }
+ diffRows() {
+ return $(this.discussion.truncatedDiffLines);
},
- methods: {
- rowTag(html) {
- return html.outerHTML ? 'tr' : 'template';
- },
+ diffFile() {
+ return convertObjectPropsToCamelCase(this.discussion.diffFile);
},
- };
+ imageDiffHtml() {
+ return this.discussion.imageDiffHtml;
+ },
+ },
+ mounted() {
+ if (this.isImageDiff) {
+ const canCreateNote = false;
+ const renderCommentBadge = true;
+ imageDiffHelper.initImageDiff(
+ this.$refs.fileHolder,
+ canCreateNote,
+ renderCommentBadge,
+ );
+ } else {
+ const fileHolder = $(this.$refs.fileHolder);
+ this.$nextTick(() => {
+ syntaxHighlight(fileHolder);
+ });
+ }
+ },
+ methods: {
+ rowTag(html) {
+ return html.outerHTML ? 'tr' : 'template';
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 0158f58b569..d492d1cd001 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,67 +1,69 @@
<script>
- import { mapGetters } from 'vuex';
- import resolveSvg from 'icons/_icon_resolve_discussion.svg';
- import resolvedSvg from 'icons/_icon_status_success_solid.svg';
- import mrIssueSvg from 'icons/_icon_mr_issue.svg';
- import nextDiscussionSvg from 'icons/_next_discussion.svg';
- import { pluralize } from '../../lib/utils/text_utility';
- import { scrollToElement } from '../../lib/utils/common_utils';
- import tooltip from '../../vue_shared/directives/tooltip';
+import { mapGetters } from 'vuex';
+import resolveSvg from 'icons/_icon_resolve_discussion.svg';
+import resolvedSvg from 'icons/_icon_status_success_solid.svg';
+import mrIssueSvg from 'icons/_icon_mr_issue.svg';
+import nextDiscussionSvg from 'icons/_next_discussion.svg';
+import { pluralize } from '../../lib/utils/text_utility';
+import { scrollToElement } from '../../lib/utils/common_utils';
+import tooltip from '../../vue_shared/directives/tooltip';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ computed: {
+ ...mapGetters([
+ 'getUserData',
+ 'getNoteableData',
+ 'discussionCount',
+ 'unresolvedDiscussions',
+ 'resolvedDiscussionCount',
+ ]),
+ isLoggedIn() {
+ return this.getUserData.id;
},
- computed: {
- ...mapGetters([
- 'getUserData',
- 'getNoteableData',
- 'discussionCount',
- 'unresolvedDiscussions',
- 'resolvedDiscussionCount',
- ]),
- isLoggedIn() {
- return this.getUserData.id;
- },
- hasNextButton() {
- return this.isLoggedIn && !this.allResolved;
- },
- countText() {
- return pluralize('discussion', this.discussionCount);
- },
- allResolved() {
- return this.resolvedDiscussionCount === this.discussionCount;
- },
- resolveAllDiscussionsIssuePath() {
- return this.getNoteableData.create_issue_to_resolve_discussions_path;
- },
- firstUnresolvedDiscussionId() {
- const item = this.unresolvedDiscussions[0] || {};
-
- return item.id;
- },
+ hasNextButton() {
+ return this.isLoggedIn && !this.allResolved;
+ },
+ countText() {
+ return pluralize('discussion', this.discussionCount);
+ },
+ allResolved() {
+ return this.resolvedDiscussionCount === this.discussionCount;
},
- created() {
- this.resolveSvg = resolveSvg;
- this.resolvedSvg = resolvedSvg;
- this.mrIssueSvg = mrIssueSvg;
- this.nextDiscussionSvg = nextDiscussionSvg;
+ resolveAllDiscussionsIssuePath() {
+ return this.getNoteableData.create_issue_to_resolve_discussions_path;
+ },
+ firstUnresolvedDiscussionId() {
+ const item = this.unresolvedDiscussions[0] || {};
+
+ return item.id;
},
- methods: {
- jumpToFirstDiscussion() {
- const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`);
- const activeTab = window.mrTabs.currentAction;
+ },
+ created() {
+ this.resolveSvg = resolveSvg;
+ this.resolvedSvg = resolvedSvg;
+ this.mrIssueSvg = mrIssueSvg;
+ this.nextDiscussionSvg = nextDiscussionSvg;
+ },
+ methods: {
+ jumpToFirstDiscussion() {
+ const el = document.querySelector(
+ `[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`,
+ );
+ const activeTab = window.mrTabs.currentAction;
- if (activeTab === 'commits' || activeTab === 'pipelines') {
- window.mrTabs.activateTab('show');
- }
+ if (activeTab === 'commits' || activeTab === 'pipelines') {
+ window.mrTabs.activateTab('show');
+ }
- if (el) {
- scrollToElement(el);
- }
- },
+ if (el) {
+ scrollToElement(el);
+ }
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index fc0722042cc..13283b187d1 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -1,15 +1,13 @@
<script>
- import Icon from '~/vue_shared/components/icon.vue';
- import Issuable from '~/vue_shared/mixins/issuable';
+import Icon from '~/vue_shared/components/icon.vue';
+import Issuable from '~/vue_shared/mixins/issuable';
- export default {
- components: {
- Icon,
- },
- mixins: [
- Issuable,
- ],
- };
+export default {
+ components: {
+ Icon,
+ },
+ mixins: [Issuable],
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index c26aa6fa15d..a7e2d857013 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,121 +1,119 @@
<script>
- import { mapGetters } from 'vuex';
- import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
- import emojiSmile from 'icons/_emoji_smile.svg';
- import emojiSmiley from 'icons/_emoji_smiley.svg';
- import editSvg from 'icons/_icon_pencil.svg';
- import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
- import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
- import ellipsisSvg from 'icons/_ellipsis_v.svg';
- import loadingIcon from '~/vue_shared/components/loading_icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
+import { mapGetters } from 'vuex';
+import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
+import emojiSmile from 'icons/_emoji_smile.svg';
+import emojiSmiley from 'icons/_emoji_smiley.svg';
+import editSvg from 'icons/_icon_pencil.svg';
+import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
+import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
+import ellipsisSvg from 'icons/_ellipsis_v.svg';
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- name: 'NoteActions',
- directives: {
- tooltip,
- },
- components: {
- loadingIcon,
- },
- props: {
- authorId: {
- type: Number,
- required: true,
- },
- noteId: {
- type: Number,
- required: true,
- },
- accessLevel: {
- type: String,
- required: false,
- default: '',
- },
- reportAbusePath: {
- type: String,
- required: true,
- },
- canEdit: {
- type: Boolean,
- required: true,
- },
- canDelete: {
- type: Boolean,
- required: true,
- },
- resolvable: {
- type: Boolean,
- required: false,
- default: false,
- },
- isResolved: {
- type: Boolean,
- required: false,
- default: false,
- },
- isResolving: {
- type: Boolean,
- required: false,
- default: false,
- },
- resolvedBy: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- canReportAsAbuse: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- ...mapGetters([
- 'getUserDataByProp',
- ]),
- shouldShowActionsDropdown() {
- return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
- },
- canAddAwardEmoji() {
- return this.currentUserId;
- },
- isAuthoredByCurrentUser() {
- return this.authorId === this.currentUserId;
- },
- currentUserId() {
- return this.getUserDataByProp('id');
- },
- resolveButtonTitle() {
- let title = 'Mark as resolved';
+export default {
+ name: 'NoteActions',
+ directives: {
+ tooltip,
+ },
+ components: {
+ loadingIcon,
+ },
+ props: {
+ authorId: {
+ type: Number,
+ required: true,
+ },
+ noteId: {
+ type: Number,
+ required: true,
+ },
+ accessLevel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ reportAbusePath: {
+ type: String,
+ required: true,
+ },
+ canEdit: {
+ type: Boolean,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: true,
+ },
+ resolvable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isResolved: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isResolving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ resolvedBy: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ canReportAsAbuse: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters(['getUserDataByProp']),
+ shouldShowActionsDropdown() {
+ return this.currentUserId && (this.canEdit || this.canReportAsAbuse);
+ },
+ canAddAwardEmoji() {
+ return this.currentUserId;
+ },
+ isAuthoredByCurrentUser() {
+ return this.authorId === this.currentUserId;
+ },
+ currentUserId() {
+ return this.getUserDataByProp('id');
+ },
+ resolveButtonTitle() {
+ let title = 'Mark as resolved';
- if (this.resolvedBy) {
- title = `Resolved by ${this.resolvedBy.name}`;
- }
+ if (this.resolvedBy) {
+ title = `Resolved by ${this.resolvedBy.name}`;
+ }
- return title;
- },
- },
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- this.editSvg = editSvg;
- this.ellipsisSvg = ellipsisSvg;
- this.resolveDiscussionSvg = resolveDiscussionSvg;
- this.resolvedDiscussionSvg = resolvedDiscussionSvg;
- },
- methods: {
- onEdit() {
- this.$emit('handleEdit');
- },
- onDelete() {
- this.$emit('handleDelete');
- },
- onResolve() {
- this.$emit('handleResolve');
- },
- },
- };
+ return title;
+ },
+ },
+ created() {
+ this.emojiSmiling = emojiSmiling;
+ this.emojiSmile = emojiSmile;
+ this.emojiSmiley = emojiSmiley;
+ this.editSvg = editSvg;
+ this.ellipsisSvg = ellipsisSvg;
+ this.resolveDiscussionSvg = resolveDiscussionSvg;
+ this.resolvedDiscussionSvg = resolvedDiscussionSvg;
+ },
+ methods: {
+ onEdit() {
+ this.$emit('handleEdit');
+ },
+ onDelete() {
+ this.$emit('handleDelete');
+ },
+ onResolve() {
+ this.$emit('handleResolve');
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/note_attachment.vue b/app/assets/javascripts/notes/components/note_attachment.vue
index 618b807b9cc..34ecbd00c63 100644
--- a/app/assets/javascripts/notes/components/note_attachment.vue
+++ b/app/assets/javascripts/notes/components/note_attachment.vue
@@ -1,13 +1,13 @@
<script>
- export default {
- name: 'NoteAttachment',
- props: {
- attachment: {
- type: Object,
- required: true,
- },
+export default {
+ name: 'NoteAttachment',
+ props: {
+ attachment: {
+ type: Object,
+ required: true,
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index caa9701e03f..6cb8229e268 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,179 +1,192 @@
<script>
- import { mapActions, mapGetters } from 'vuex';
- import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
- import emojiSmile from 'icons/_emoji_smile.svg';
- import emojiSmiley from 'icons/_emoji_smiley.svg';
- import Flash from '../../flash';
- import { glEmojiTag } from '../../emoji';
- import tooltip from '../../vue_shared/directives/tooltip';
-
- export default {
- directives: {
- tooltip,
+import { mapActions, mapGetters } from 'vuex';
+import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
+import emojiSmile from 'icons/_emoji_smile.svg';
+import emojiSmiley from 'icons/_emoji_smiley.svg';
+import Flash from '../../flash';
+import { glEmojiTag } from '../../emoji';
+import tooltip from '../../vue_shared/directives/tooltip';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ awards: {
+ type: Array,
+ required: true,
},
- props: {
- awards: {
- type: Array,
- required: true,
- },
- toggleAwardPath: {
- type: String,
- required: true,
- },
- noteAuthorId: {
- type: Number,
- required: true,
- },
- noteId: {
- type: Number,
- required: true,
- },
+ toggleAwardPath: {
+ type: String,
+ required: true,
},
- computed: {
- ...mapGetters([
- 'getUserData',
- ]),
- // `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
- // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
- // This method will group emojis by their name as an Object. See below.
- // {
- // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ],
- // bar: [ { name: bar, user: user1 } ]
- // }
- // We need to do this otherwise we will render the same emoji over and over again.
- groupedAwards() {
- const awards = this.awards.reduce((acc, award) => {
- if (Object.prototype.hasOwnProperty.call(acc, award.name)) {
- acc[award.name].push(award);
- } else {
- Object.assign(acc, { [award.name]: [award] });
- }
-
- return acc;
- }, {});
-
- const orderedAwards = {};
- const { thumbsdown, thumbsup } = awards;
- // Always show thumbsup and thumbsdown first
- if (thumbsup) {
- orderedAwards.thumbsup = thumbsup;
- delete awards.thumbsup;
- }
- if (thumbsdown) {
- orderedAwards.thumbsdown = thumbsdown;
- delete awards.thumbsdown;
- }
-
- return Object.assign({}, orderedAwards, awards);
- },
- isAuthoredByMe() {
- return this.noteAuthorId === this.getUserData.id;
- },
- isLoggedIn() {
- return this.getUserData.id;
- },
+ noteAuthorId: {
+ type: Number,
+ required: true,
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
+ noteId: {
+ type: Number,
+ required: true,
},
- methods: {
- ...mapActions([
- 'toggleAwardRequest',
- ]),
- getAwardHTML(name) {
- return glEmojiTag(name);
- },
- getAwardClassBindings(awardList, awardName) {
- return {
- active: this.hasReactionByCurrentUser(awardList),
- disabled: !this.canInteractWithEmoji(awardList, awardName),
- };
- },
- canInteractWithEmoji(awardList, awardName) {
- let isAllowed = true;
- const restrictedEmojis = ['thumbsup', 'thumbsdown'];
-
- // Users can not add :+1: and :-1: to their own notes
- if (this.getUserData.id === this.noteAuthorId && restrictedEmojis.indexOf(awardName) > -1) {
- isAllowed = false;
- }
-
- return this.getUserData.id && isAllowed;
- },
- hasReactionByCurrentUser(awardList) {
- return awardList.filter(award => award.user.id === this.getUserData.id).length;
- },
- awardTitle(awardsList) {
- const hasReactionByCurrentUser = this.hasReactionByCurrentUser(awardsList);
- const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
- let awardList = awardsList;
-
- // Filter myself from list if I am awarded.
- if (hasReactionByCurrentUser) {
- awardList = awardList.filter(award => award.user.id !== this.getUserData.id);
- }
-
- // Get only 9-10 usernames to show in tooltip text.
- const namesToShow = awardList.slice(0, TOOLTIP_NAME_COUNT).map(award => award.user.name);
-
- // Get the remaining list to use in `and x more` text.
- const remainingAwardList = awardList.slice(TOOLTIP_NAME_COUNT, awardList.length);
-
- // Add myself to the begining of the list so title will start with You.
- if (hasReactionByCurrentUser) {
- namesToShow.unshift('You');
- }
-
- let title = '';
-
- // We have 10+ awarded user, join them with comma and add `and x more`.
- if (remainingAwardList.length) {
- title = `${namesToShow.join(', ')}, and ${remainingAwardList.length} more.`;
- } else if (namesToShow.length > 1) {
- // Join all names with comma but not the last one, it will be added with and text.
- title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
- // If we have more than 2 users we need an extra comma before and text.
- title += namesToShow.length > 2 ? ',' : '';
- title += ` and ${namesToShow.slice(-1)}`; // Append and text
- } else { // We have only 2 users so join them with and.
- title = namesToShow.join(' and ');
- }
-
- return title;
- },
- handleAward(awardName) {
- if (!this.isLoggedIn) {
- return;
- }
-
- let parsedName;
-
- // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string
- switch (awardName) {
- case '100':
- parsedName = 100;
- break;
- case '1234':
- parsedName = 1234;
- break;
- default:
- parsedName = awardName;
- break;
+ },
+ computed: {
+ ...mapGetters(['getUserData']),
+ // `this.awards` is an array with emojis but they are not grouped by emoji name. See below.
+ // [ { name: foo, user: user1 }, { name: bar, user: user1 }, { name: foo, user: user2 } ]
+ // This method will group emojis by their name as an Object. See below.
+ // {
+ // foo: [ { name: foo, user: user1 }, { name: foo, user: user2 } ],
+ // bar: [ { name: bar, user: user1 } ]
+ // }
+ // We need to do this otherwise we will render the same emoji over and over again.
+ groupedAwards() {
+ const awards = this.awards.reduce((acc, award) => {
+ if (Object.prototype.hasOwnProperty.call(acc, award.name)) {
+ acc[award.name].push(award);
+ } else {
+ Object.assign(acc, { [award.name]: [award] });
}
- const data = {
- endpoint: this.toggleAwardPath,
- noteId: this.noteId,
- awardName: parsedName,
- };
-
- this.toggleAwardRequest(data)
- .catch(() => Flash('Something went wrong on our end.'));
- },
+ return acc;
+ }, {});
+
+ const orderedAwards = {};
+ const { thumbsdown, thumbsup } = awards;
+ // Always show thumbsup and thumbsdown first
+ if (thumbsup) {
+ orderedAwards.thumbsup = thumbsup;
+ delete awards.thumbsup;
+ }
+ if (thumbsdown) {
+ orderedAwards.thumbsdown = thumbsdown;
+ delete awards.thumbsdown;
+ }
+
+ return Object.assign({}, orderedAwards, awards);
+ },
+ isAuthoredByMe() {
+ return this.noteAuthorId === this.getUserData.id;
+ },
+ isLoggedIn() {
+ return this.getUserData.id;
+ },
+ },
+ created() {
+ this.emojiSmiling = emojiSmiling;
+ this.emojiSmile = emojiSmile;
+ this.emojiSmiley = emojiSmiley;
+ },
+ methods: {
+ ...mapActions(['toggleAwardRequest']),
+ getAwardHTML(name) {
+ return glEmojiTag(name);
+ },
+ getAwardClassBindings(awardList, awardName) {
+ return {
+ active: this.hasReactionByCurrentUser(awardList),
+ disabled: !this.canInteractWithEmoji(awardList, awardName),
+ };
+ },
+ canInteractWithEmoji(awardList, awardName) {
+ let isAllowed = true;
+ const restrictedEmojis = ['thumbsup', 'thumbsdown'];
+
+ // Users can not add :+1: and :-1: to their own notes
+ if (
+ this.getUserData.id === this.noteAuthorId &&
+ restrictedEmojis.indexOf(awardName) > -1
+ ) {
+ isAllowed = false;
+ }
+
+ return this.getUserData.id && isAllowed;
+ },
+ hasReactionByCurrentUser(awardList) {
+ return awardList.filter(award => award.user.id === this.getUserData.id)
+ .length;
+ },
+ awardTitle(awardsList) {
+ const hasReactionByCurrentUser = this.hasReactionByCurrentUser(
+ awardsList,
+ );
+ const TOOLTIP_NAME_COUNT = hasReactionByCurrentUser ? 9 : 10;
+ let awardList = awardsList;
+
+ // Filter myself from list if I am awarded.
+ if (hasReactionByCurrentUser) {
+ awardList = awardList.filter(
+ award => award.user.id !== this.getUserData.id,
+ );
+ }
+
+ // Get only 9-10 usernames to show in tooltip text.
+ const namesToShow = awardList
+ .slice(0, TOOLTIP_NAME_COUNT)
+ .map(award => award.user.name);
+
+ // Get the remaining list to use in `and x more` text.
+ const remainingAwardList = awardList.slice(
+ TOOLTIP_NAME_COUNT,
+ awardList.length,
+ );
+
+ // Add myself to the begining of the list so title will start with You.
+ if (hasReactionByCurrentUser) {
+ namesToShow.unshift('You');
+ }
+
+ let title = '';
+
+ // We have 10+ awarded user, join them with comma and add `and x more`.
+ if (remainingAwardList.length) {
+ title = `${namesToShow.join(', ')}, and ${
+ remainingAwardList.length
+ } more.`;
+ } else if (namesToShow.length > 1) {
+ // Join all names with comma but not the last one, it will be added with and text.
+ title = namesToShow.slice(0, namesToShow.length - 1).join(', ');
+ // If we have more than 2 users we need an extra comma before and text.
+ title += namesToShow.length > 2 ? ',' : '';
+ title += ` and ${namesToShow.slice(-1)}`; // Append and text
+ } else {
+ // We have only 2 users so join them with and.
+ title = namesToShow.join(' and ');
+ }
+
+ return title;
+ },
+ handleAward(awardName) {
+ if (!this.isLoggedIn) {
+ return;
+ }
+
+ let parsedName;
+
+ // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string
+ switch (awardName) {
+ case '100':
+ parsedName = 100;
+ break;
+ case '1234':
+ parsedName = 1234;
+ break;
+ default:
+ parsedName = awardName;
+ break;
+ }
+
+ const data = {
+ endpoint: this.toggleAwardPath,
+ noteId: this.noteId,
+ awardName: parsedName,
+ };
+
+ this.toggleAwardRequest(data).catch(() =>
+ Flash('Something went wrong on our end.'),
+ );
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index a94f1a28a4c..069f94c5845 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,83 +1,81 @@
<script>
- import $ from 'jquery';
- import noteEditedText from './note_edited_text.vue';
- import noteAwardsList from './note_awards_list.vue';
- import noteAttachment from './note_attachment.vue';
- import noteForm from './note_form.vue';
- import TaskList from '../../task_list';
- import autosave from '../mixins/autosave';
+import $ from 'jquery';
+import noteEditedText from './note_edited_text.vue';
+import noteAwardsList from './note_awards_list.vue';
+import noteAttachment from './note_attachment.vue';
+import noteForm from './note_form.vue';
+import TaskList from '../../task_list';
+import autosave from '../mixins/autosave';
- export default {
- components: {
- noteEditedText,
- noteAwardsList,
- noteAttachment,
- noteForm,
+export default {
+ components: {
+ noteEditedText,
+ noteAwardsList,
+ noteAttachment,
+ noteForm,
+ },
+ mixins: [autosave],
+ props: {
+ note: {
+ type: Object,
+ required: true,
},
- mixins: [
- autosave,
- ],
- props: {
- note: {
- type: Object,
- required: true,
- },
- canEdit: {
- type: Boolean,
- required: true,
- },
- isEditing: {
- type: Boolean,
- required: false,
- default: false,
- },
+ canEdit: {
+ type: Boolean,
+ required: true,
},
- computed: {
- noteBody() {
- return this.note.note;
- },
+ isEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- mounted() {
- this.renderGFM();
- this.initTaskList();
+ },
+ computed: {
+ noteBody() {
+ return this.note.note;
+ },
+ },
+ mounted() {
+ this.renderGFM();
+ this.initTaskList();
+
+ if (this.isEditing) {
+ this.initAutoSave(this.note.noteable_type);
+ }
+ },
+ updated() {
+ this.initTaskList();
+ this.renderGFM();
- if (this.isEditing) {
+ if (this.isEditing) {
+ if (!this.autosave) {
this.initAutoSave(this.note.noteable_type);
+ } else {
+ this.setAutoSave();
}
+ }
+ },
+ methods: {
+ renderGFM() {
+ $(this.$refs['note-body']).renderGFM();
},
- updated() {
- this.initTaskList();
- this.renderGFM();
-
- if (this.isEditing) {
- if (!this.autosave) {
- this.initAutoSave(this.note.noteable_type);
- } else {
- this.setAutoSave();
- }
+ initTaskList() {
+ if (this.canEdit) {
+ this.taskList = new TaskList({
+ dataType: 'note',
+ fieldName: 'note',
+ selector: '.notes',
+ });
}
},
- methods: {
- renderGFM() {
- $(this.$refs['note-body']).renderGFM();
- },
- initTaskList() {
- if (this.canEdit) {
- this.taskList = new TaskList({
- dataType: 'note',
- fieldName: 'note',
- selector: '.notes',
- });
- }
- },
- handleFormUpdate(note, parentElement, callback) {
- this.$emit('handleFormUpdate', note, parentElement, callback);
- },
- formCancelHandler(shouldConfirm, isDirty) {
- this.$emit('cancelFormEdition', shouldConfirm, isDirty);
- },
+ handleFormUpdate(note, parentElement, callback) {
+ this.$emit('handleFormUpdate', note, parentElement, callback);
+ },
+ formCancelHandler(shouldConfirm, isDirty) {
+ this.$emit('cancelFormEdition', shouldConfirm, isDirty);
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/note_edited_text.vue b/app/assets/javascripts/notes/components/note_edited_text.vue
index ae2e52554d2..4ddca918495 100644
--- a/app/assets/javascripts/notes/components/note_edited_text.vue
+++ b/app/assets/javascripts/notes/components/note_edited_text.vue
@@ -1,32 +1,32 @@
<script>
- import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
+import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
- export default {
- name: 'EditedNoteText',
- components: {
- timeAgoTooltip,
+export default {
+ name: 'EditedNoteText',
+ components: {
+ timeAgoTooltip,
+ },
+ props: {
+ actionText: {
+ type: String,
+ required: true,
},
- props: {
- actionText: {
- type: String,
- required: true,
- },
- editedAt: {
- type: String,
- required: true,
- },
- editedBy: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- className: {
- type: String,
- required: false,
- default: 'edited-text',
- },
+ editedAt: {
+ type: String,
+ required: true,
},
- };
+ editedBy: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ className: {
+ type: String,
+ required: false,
+ default: 'edited-text',
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 1a13fdbeb7c..c59a2e7a406 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,128 +1,136 @@
<script>
- import { mapGetters, mapActions } from 'vuex';
- import eventHub from '../event_hub';
- import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
- import markdownField from '../../vue_shared/components/markdown/field.vue';
- import issuableStateMixin from '../mixins/issuable_state';
- import resolvable from '../mixins/resolvable';
+import { mapGetters, mapActions } from 'vuex';
+import eventHub from '../event_hub';
+import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
+import markdownField from '../../vue_shared/components/markdown/field.vue';
+import issuableStateMixin from '../mixins/issuable_state';
+import resolvable from '../mixins/resolvable';
- export default {
- name: 'IssueNoteForm',
- components: {
- issueWarning,
- markdownField,
+export default {
+ name: 'IssueNoteForm',
+ components: {
+ issueWarning,
+ markdownField,
+ },
+ mixins: [issuableStateMixin, resolvable],
+ props: {
+ noteBody: {
+ type: String,
+ required: false,
+ default: '',
},
- mixins: [
- issuableStateMixin,
- resolvable,
- ],
- props: {
- noteBody: {
- type: String,
- required: false,
- default: '',
- },
- noteId: {
- type: Number,
- required: false,
- default: 0,
- },
- saveButtonTitle: {
- type: String,
- required: false,
- default: 'Save comment',
- },
- note: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- isEditing: {
- type: Boolean,
- required: true,
- },
+ noteId: {
+ type: Number,
+ required: false,
+ default: 0,
},
- data() {
- return {
- updatedNoteBody: this.noteBody,
- conflictWhileEditing: false,
- isSubmitting: false,
- isResolving: false,
- resolveAsThread: true,
- };
+ saveButtonTitle: {
+ type: String,
+ required: false,
+ default: 'Save comment',
},
- computed: {
- ...mapGetters([
- 'getDiscussionLastNote',
- 'getNoteableData',
- 'getNoteableDataByProp',
- 'getNotesDataByProp',
- 'getUserDataByProp',
- ]),
- noteHash() {
- return `#note_${this.noteId}`;
- },
- markdownPreviewPath() {
- return this.getNoteableDataByProp('preview_note_path');
- },
- markdownDocsPath() {
- return this.getNotesDataByProp('markdownDocsPath');
- },
- quickActionsDocsPath() {
- return !this.isEditing ? this.getNotesDataByProp('quickActionsDocsPath') : undefined;
- },
- currentUserId() {
- return this.getUserDataByProp('id');
- },
- isDisabled() {
- return !this.updatedNoteBody.length || this.isSubmitting;
- },
+ note: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- watch: {
- noteBody() {
- if (this.updatedNoteBody === this.noteBody) {
- this.updatedNoteBody = this.noteBody;
- } else {
- this.conflictWhileEditing = true;
- }
- },
+ isEditing: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ updatedNoteBody: this.noteBody,
+ conflictWhileEditing: false,
+ isSubmitting: false,
+ isResolving: false,
+ resolveAsThread: true,
+ };
+ },
+ computed: {
+ ...mapGetters([
+ 'getDiscussionLastNote',
+ 'getNoteableData',
+ 'getNoteableDataByProp',
+ 'getNotesDataByProp',
+ 'getUserDataByProp',
+ ]),
+ noteHash() {
+ return `#note_${this.noteId}`;
+ },
+ markdownPreviewPath() {
+ return this.getNoteableDataByProp('preview_note_path');
+ },
+ markdownDocsPath() {
+ return this.getNotesDataByProp('markdownDocsPath');
+ },
+ quickActionsDocsPath() {
+ return !this.isEditing
+ ? this.getNotesDataByProp('quickActionsDocsPath')
+ : undefined;
},
- mounted() {
- this.$refs.textarea.focus();
+ currentUserId() {
+ return this.getUserDataByProp('id');
},
- methods: {
- ...mapActions([
- 'toggleResolveNote',
- ]),
- handleUpdate(shouldResolve) {
- const beforeSubmitDiscussionState = this.discussionResolved;
- this.isSubmitting = true;
+ isDisabled() {
+ return !this.updatedNoteBody.length || this.isSubmitting;
+ },
+ },
+ watch: {
+ noteBody() {
+ if (this.updatedNoteBody === this.noteBody) {
+ this.updatedNoteBody = this.noteBody;
+ } else {
+ this.conflictWhileEditing = true;
+ }
+ },
+ },
+ mounted() {
+ this.$refs.textarea.focus();
+ },
+ methods: {
+ ...mapActions(['toggleResolveNote']),
+ handleUpdate(shouldResolve) {
+ const beforeSubmitDiscussionState = this.discussionResolved;
+ this.isSubmitting = true;
- this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
+ this.$emit(
+ 'handleFormUpdate',
+ this.updatedNoteBody,
+ this.$refs.editNoteForm,
+ () => {
this.isSubmitting = false;
if (shouldResolve) {
this.resolveHandler(beforeSubmitDiscussionState);
}
- });
- },
- editMyLastNote() {
- if (this.updatedNoteBody === '') {
- const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody);
+ },
+ );
+ },
+ editMyLastNote() {
+ if (this.updatedNoteBody === '') {
+ const lastNoteInDiscussion = this.getDiscussionLastNote(
+ this.updatedNoteBody,
+ );
- if (lastNoteInDiscussion) {
- eventHub.$emit('enterEditMode', {
- noteId: lastNoteInDiscussion.id,
- });
- }
+ if (lastNoteInDiscussion) {
+ eventHub.$emit('enterEditMode', {
+ noteId: lastNoteInDiscussion.id,
+ });
}
- },
- cancelHandler(shouldConfirm = false) {
- // Sends information about confirm message and if the textarea has changed
- this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody);
- },
+ }
+ },
+ cancelHandler(shouldConfirm = false) {
+ // Sends information about confirm message and if the textarea has changed
+ this.$emit(
+ 'cancelFormEdition',
+ shouldConfirm,
+ this.noteBody !== this.updatedNoteBody,
+ );
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 4743d95b951..c3d1ef1fcc6 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,65 +1,63 @@
<script>
- import { mapActions } from 'vuex';
- import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
+import { mapActions } from 'vuex';
+import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
- export default {
- components: {
- timeAgoTooltip,
+export default {
+ components: {
+ timeAgoTooltip,
+ },
+ props: {
+ author: {
+ type: Object,
+ required: true,
},
- props: {
- author: {
- type: Object,
- required: true,
- },
- createdAt: {
- type: String,
- required: true,
- },
- actionText: {
- type: String,
- required: false,
- default: '',
- },
- actionTextHtml: {
- type: String,
- required: false,
- default: '',
- },
- noteId: {
- type: Number,
- required: true,
- },
- includeToggle: {
- type: Boolean,
- required: false,
- default: false,
- },
- expanded: {
- type: Boolean,
- required: false,
- default: true,
- },
+ createdAt: {
+ type: String,
+ required: true,
},
- computed: {
- toggleChevronClass() {
- return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
- },
- noteTimestampLink() {
- return `#note_${this.noteId}`;
- },
+ actionText: {
+ type: String,
+ required: false,
+ default: '',
},
- methods: {
- ...mapActions([
- 'setTargetNoteHash',
- ]),
- handleToggle() {
- this.$emit('toggleHandler');
- },
- updateTargetNoteHash() {
- this.setTargetNoteHash(this.noteTimestampLink);
- },
+ actionTextHtml: {
+ type: String,
+ required: false,
+ default: '',
},
- };
+ noteId: {
+ type: Number,
+ required: true,
+ },
+ includeToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ expanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ toggleChevronClass() {
+ return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
+ },
+ noteTimestampLink() {
+ return `#note_${this.noteId}`;
+ },
+ },
+ methods: {
+ ...mapActions(['setTargetNoteHash']),
+ handleToggle() {
+ this.$emit('toggleHandler');
+ },
+ updateTargetNoteHash() {
+ this.setTargetNoteHash(this.noteTimestampLink);
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index 45d3c2de355..91f7c269757 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -1,19 +1,17 @@
<script>
- import { mapGetters } from 'vuex';
+import { mapGetters } from 'vuex';
- export default {
- computed: {
- ...mapGetters([
- 'getNotesDataByProp',
- ]),
- registerLink() {
- return this.getNotesDataByProp('registerPath');
- },
- signInLink() {
- return this.getNotesDataByProp('newSessionPath');
- },
+export default {
+ computed: {
+ ...mapGetters(['getNotesDataByProp']),
+ registerLink() {
+ return this.getNotesDataByProp('registerPath');
},
- };
+ signInLink() {
+ return this.getNotesDataByProp('newSessionPath');
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 76bb53eaf2f..cf579c5d4dc 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,210 +1,210 @@
<script>
- import { mapActions, mapGetters } from 'vuex';
- import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
- import nextDiscussionsSvg from 'icons/_next_discussion.svg';
- import Flash from '../../flash';
- import { SYSTEM_NOTE } from '../constants';
- import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import noteableNote from './noteable_note.vue';
- import noteHeader from './note_header.vue';
- import noteSignedOutWidget from './note_signed_out_widget.vue';
- import noteEditedText from './note_edited_text.vue';
- import noteForm from './note_form.vue';
- import diffWithNote from './diff_with_note.vue';
- import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
- import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
- import autosave from '../mixins/autosave';
- import noteable from '../mixins/noteable';
- import resolvable from '../mixins/resolvable';
- import tooltip from '../../vue_shared/directives/tooltip';
- import { scrollToElement } from '../../lib/utils/common_utils';
+import { mapActions, mapGetters } from 'vuex';
+import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
+import nextDiscussionsSvg from 'icons/_next_discussion.svg';
+import Flash from '../../flash';
+import { SYSTEM_NOTE } from '../constants';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import noteableNote from './noteable_note.vue';
+import noteHeader from './note_header.vue';
+import noteSignedOutWidget from './note_signed_out_widget.vue';
+import noteEditedText from './note_edited_text.vue';
+import noteForm from './note_form.vue';
+import diffWithNote from './diff_with_note.vue';
+import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
+import autosave from '../mixins/autosave';
+import noteable from '../mixins/noteable';
+import resolvable from '../mixins/resolvable';
+import tooltip from '../../vue_shared/directives/tooltip';
+import { scrollToElement } from '../../lib/utils/common_utils';
- export default {
- components: {
- noteableNote,
- diffWithNote,
- userAvatarLink,
- noteHeader,
- noteSignedOutWidget,
- noteEditedText,
- noteForm,
- placeholderNote,
- placeholderSystemNote,
+export default {
+ components: {
+ noteableNote,
+ diffWithNote,
+ userAvatarLink,
+ noteHeader,
+ noteSignedOutWidget,
+ noteEditedText,
+ noteForm,
+ placeholderNote,
+ placeholderSystemNote,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [autosave, noteable, resolvable],
+ props: {
+ note: {
+ type: Object,
+ required: true,
},
- directives: {
- tooltip,
- },
- mixins: [
- autosave,
- noteable,
- resolvable,
- ],
- props: {
- note: {
- type: Object,
- required: true,
- },
- },
- data() {
+ },
+ data() {
+ return {
+ isReplying: false,
+ isResolving: false,
+ resolveAsThread: true,
+ };
+ },
+ computed: {
+ ...mapGetters([
+ 'getNoteableData',
+ 'discussionCount',
+ 'resolvedDiscussionCount',
+ 'unresolvedDiscussions',
+ ]),
+ discussion() {
return {
- isReplying: false,
- isResolving: false,
- resolveAsThread: true,
+ ...this.note.notes[0],
+ truncatedDiffLines: this.note.truncated_diff_lines,
+ diffFile: this.note.diff_file,
+ diffDiscussion: this.note.diff_discussion,
+ imageDiffHtml: this.note.image_diff_html,
};
},
- computed: {
- ...mapGetters([
- 'getNoteableData',
- 'discussionCount',
- 'resolvedDiscussionCount',
- 'unresolvedDiscussions',
- ]),
- discussion() {
- return {
- ...this.note.notes[0],
- truncatedDiffLines: this.note.truncated_diff_lines,
- diffFile: this.note.diff_file,
- diffDiscussion: this.note.diff_discussion,
- imageDiffHtml: this.note.image_diff_html,
- };
- },
- author() {
- return this.discussion.author;
- },
- canReply() {
- return this.getNoteableData.current_user.can_create_note;
- },
- newNotePath() {
- return this.getNoteableData.create_note_path;
- },
- lastUpdatedBy() {
- const { notes } = this.note;
+ author() {
+ return this.discussion.author;
+ },
+ canReply() {
+ return this.getNoteableData.current_user.can_create_note;
+ },
+ newNotePath() {
+ return this.getNoteableData.create_note_path;
+ },
+ lastUpdatedBy() {
+ const { notes } = this.note;
- if (notes.length > 1) {
- return notes[notes.length - 1].author;
- }
+ if (notes.length > 1) {
+ return notes[notes.length - 1].author;
+ }
- return null;
- },
- lastUpdatedAt() {
- const { notes } = this.note;
+ return null;
+ },
+ lastUpdatedAt() {
+ const { notes } = this.note;
- if (notes.length > 1) {
- return notes[notes.length - 1].created_at;
- }
+ if (notes.length > 1) {
+ return notes[notes.length - 1].created_at;
+ }
- return null;
- },
- hasUnresolvedDiscussion() {
- return this.unresolvedDiscussions.length > 0;
- },
- wrapperComponent() {
- return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div';
- },
- wrapperClass() {
- return this.isDiffDiscussion ? '' : 'panel panel-default';
- },
+ return null;
+ },
+ hasUnresolvedDiscussion() {
+ return this.unresolvedDiscussions.length > 0;
+ },
+ wrapperComponent() {
+ return this.discussion.diffDiscussion && this.discussion.diffFile
+ ? diffWithNote
+ : 'div';
},
- mounted() {
- if (this.isReplying) {
+ wrapperClass() {
+ return this.isDiffDiscussion ? '' : 'panel panel-default';
+ },
+ },
+ mounted() {
+ if (this.isReplying) {
+ this.initAutoSave(this.discussion.noteable_type);
+ }
+ },
+ updated() {
+ if (this.isReplying) {
+ if (!this.autosave) {
this.initAutoSave(this.discussion.noteable_type);
+ } else {
+ this.setAutoSave();
}
- },
- updated() {
- if (this.isReplying) {
- if (!this.autosave) {
- this.initAutoSave(this.discussion.noteable_type);
- } else {
- this.setAutoSave();
+ }
+ },
+ created() {
+ this.resolveDiscussionsSvg = resolveDiscussionsSvg;
+ this.nextDiscussionsSvg = nextDiscussionsSvg;
+ },
+ methods: {
+ ...mapActions([
+ 'saveNote',
+ 'toggleDiscussion',
+ 'removePlaceholderNotes',
+ 'toggleResolveNote',
+ ]),
+ componentName(note) {
+ if (note.isPlaceholderNote) {
+ if (note.placeholderType === SYSTEM_NOTE) {
+ return placeholderSystemNote;
}
+ return placeholderNote;
}
+
+ return noteableNote;
},
- created() {
- this.resolveDiscussionsSvg = resolveDiscussionsSvg;
- this.nextDiscussionsSvg = nextDiscussionsSvg;
+ componentData(note) {
+ return note.isPlaceholderNote ? this.note.notes[0] : note;
},
- methods: {
- ...mapActions([
- 'saveNote',
- 'toggleDiscussion',
- 'removePlaceholderNotes',
- 'toggleResolveNote',
- ]),
- componentName(note) {
- if (note.isPlaceholderNote) {
- if (note.placeholderType === SYSTEM_NOTE) {
- return placeholderSystemNote;
- }
- return placeholderNote;
- }
+ toggleDiscussionHandler() {
+ this.toggleDiscussion({ discussionId: this.note.id });
+ },
+ showReplyForm() {
+ this.isReplying = true;
+ },
+ cancelReplyForm(shouldConfirm) {
+ if (shouldConfirm && this.$refs.noteForm.isDirty) {
+ const msg = 'Are you sure you want to cancel creating this comment?';
- return noteableNote;
- },
- componentData(note) {
- return note.isPlaceholderNote ? this.note.notes[0] : note;
- },
- toggleDiscussionHandler() {
- this.toggleDiscussion({ discussionId: this.note.id });
- },
- showReplyForm() {
- this.isReplying = true;
- },
- cancelReplyForm(shouldConfirm) {
- if (shouldConfirm && this.$refs.noteForm.isDirty) {
- // eslint-disable-next-line no-alert
- if (!confirm('Are you sure you want to cancel creating this comment?')) {
- return;
- }
+ // eslint-disable-next-line no-alert
+ if (!confirm(msg)) {
+ return;
}
+ }
- this.resetAutoSave();
- this.isReplying = false;
- },
- saveReply(noteText, form, callback) {
- const replyData = {
- endpoint: this.newNotePath,
- flashContainer: this.$el,
- data: {
- in_reply_to_discussion_id: this.note.reply_id,
- target_type: this.noteableType,
- target_id: this.discussion.noteable_id,
- note: { note: noteText },
- },
- };
- this.isReplying = false;
+ this.resetAutoSave();
+ this.isReplying = false;
+ },
+ saveReply(noteText, form, callback) {
+ const replyData = {
+ endpoint: this.newNotePath,
+ flashContainer: this.$el,
+ data: {
+ in_reply_to_discussion_id: this.note.reply_id,
+ target_type: this.noteableType,
+ target_id: this.discussion.noteable_id,
+ note: { note: noteText },
+ },
+ };
+ this.isReplying = false;
- this.saveNote(replyData)
- .then(() => {
- this.resetAutoSave();
- callback();
- })
- .catch((err) => {
- this.removePlaceholderNotes();
- this.isReplying = true;
- this.$nextTick(() => {
- const msg = `Your comment could not be submitted!
+ this.saveNote(replyData)
+ .then(() => {
+ this.resetAutoSave();
+ callback();
+ })
+ .catch(err => {
+ this.removePlaceholderNotes();
+ this.isReplying = true;
+ this.$nextTick(() => {
+ const msg = `Your comment could not be submitted!
Please check your network connection and try again.`;
- Flash(msg, 'alert', this.$el);
- this.$refs.noteForm.note = noteText;
- callback(err);
- });
+ Flash(msg, 'alert', this.$el);
+ this.$refs.noteForm.note = noteText;
+ callback(err);
});
- },
- jumpToDiscussion() {
- const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
- const index = unresolvedIds.indexOf(this.note.id);
+ });
+ },
+ jumpToDiscussion() {
+ const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
+ const index = unresolvedIds.indexOf(this.note.id);
- if (index >= 0 && index !== unresolvedIds.length) {
- const nextId = unresolvedIds[index + 1];
- const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
+ if (index >= 0 && index !== unresolvedIds.length) {
+ const nextId = unresolvedIds[index + 1];
+ const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
- if (el) {
- scrollToElement(el);
- }
+ if (el) {
+ scrollToElement(el);
}
- },
+ }
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 6d5501d7d98..3554027d2b4 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -1,152 +1,152 @@
<script>
- import $ from 'jquery';
- import { mapGetters, mapActions } from 'vuex';
- import { escape } from 'underscore';
- import Flash from '../../flash';
- import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
- import noteHeader from './note_header.vue';
- import noteActions from './note_actions.vue';
- import noteBody from './note_body.vue';
- import eventHub from '../event_hub';
- import noteable from '../mixins/noteable';
- import resolvable from '../mixins/resolvable';
+import $ from 'jquery';
+import { mapGetters, mapActions } from 'vuex';
+import { escape } from 'underscore';
+import Flash from '../../flash';
+import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+import noteHeader from './note_header.vue';
+import noteActions from './note_actions.vue';
+import noteBody from './note_body.vue';
+import eventHub from '../event_hub';
+import noteable from '../mixins/noteable';
+import resolvable from '../mixins/resolvable';
- export default {
- components: {
- userAvatarLink,
- noteHeader,
- noteActions,
- noteBody,
+export default {
+ components: {
+ userAvatarLink,
+ noteHeader,
+ noteActions,
+ noteBody,
+ },
+ mixins: [noteable, resolvable],
+ props: {
+ note: {
+ type: Object,
+ required: true,
},
- mixins: [
- noteable,
- resolvable,
- ],
- props: {
- note: {
- type: Object,
- required: true,
- },
+ },
+ data() {
+ return {
+ isEditing: false,
+ isDeleting: false,
+ isRequesting: false,
+ isResolving: false,
+ };
+ },
+ computed: {
+ ...mapGetters(['targetNoteHash', 'getUserData']),
+ author() {
+ return this.note.author;
},
- data() {
+ classNameBindings() {
return {
- isEditing: false,
- isDeleting: false,
- isRequesting: false,
- isResolving: false,
+ 'is-editing': this.isEditing && !this.isRequesting,
+ 'is-requesting being-posted': this.isRequesting,
+ 'disabled-content': this.isDeleting,
+ target: this.targetNoteHash === this.noteAnchorId,
};
},
- computed: {
- ...mapGetters([
- 'targetNoteHash',
- 'getUserData',
- ]),
- author() {
- return this.note.author;
- },
- classNameBindings() {
- return {
- 'is-editing': this.isEditing && !this.isRequesting,
- 'is-requesting being-posted': this.isRequesting,
- 'disabled-content': this.isDeleting,
- target: this.targetNoteHash === this.noteAnchorId,
- };
- },
- canReportAsAbuse() {
- return this.note.report_abuse_path && this.author.id !== this.getUserData.id;
- },
- noteAnchorId() {
- return `note_${this.note.id}`;
- },
+ canReportAsAbuse() {
+ return (
+ this.note.report_abuse_path && this.author.id !== this.getUserData.id
+ );
},
-
- created() {
- eventHub.$on('enterEditMode', ({ noteId }) => {
- if (noteId === this.note.id) {
- this.isEditing = true;
- this.scrollToNoteIfNeeded($(this.$el));
- }
- });
+ noteAnchorId() {
+ return `note_${this.note.id}`;
},
+ },
- methods: {
- ...mapActions([
- 'deleteNote',
- 'updateNote',
- 'toggleResolveNote',
- 'scrollToNoteIfNeeded',
- ]),
- editHandler() {
+ created() {
+ eventHub.$on('enterEditMode', ({ noteId }) => {
+ if (noteId === this.note.id) {
this.isEditing = true;
- },
- deleteHandler() {
- // eslint-disable-next-line no-alert
- if (confirm('Are you sure you want to delete this comment?')) {
- this.isDeleting = true;
+ this.scrollToNoteIfNeeded($(this.$el));
+ }
+ });
+ },
- this.deleteNote(this.note)
- .then(() => {
- this.isDeleting = false;
- })
- .catch(() => {
- Flash('Something went wrong while deleting your note. Please try again.');
- this.isDeleting = false;
- });
- }
- },
- formUpdateHandler(noteText, parentElement, callback) {
- const data = {
- endpoint: this.note.path,
- note: {
- target_type: this.noteableType,
- target_id: this.note.noteable_id,
- note: { note: noteText },
- },
- };
- this.isRequesting = true;
- this.oldContent = this.note.note_html;
- this.note.note_html = escape(noteText);
+ methods: {
+ ...mapActions([
+ 'deleteNote',
+ 'updateNote',
+ 'toggleResolveNote',
+ 'scrollToNoteIfNeeded',
+ ]),
+ editHandler() {
+ this.isEditing = true;
+ },
+ deleteHandler() {
+ // eslint-disable-next-line no-alert
+ if (confirm('Are you sure you want to delete this comment?')) {
+ this.isDeleting = true;
- this.updateNote(data)
+ this.deleteNote(this.note)
.then(() => {
- this.isEditing = false;
- this.isRequesting = false;
- this.oldContent = null;
- $(this.$refs.noteBody.$el).renderGFM();
- this.$refs.noteBody.resetAutoSave();
- callback();
+ this.isDeleting = false;
})
.catch(() => {
- this.isRequesting = false;
- this.isEditing = true;
- this.$nextTick(() => {
- const msg = 'Something went wrong while editing your comment. Please try again.';
- Flash(msg, 'alert', this.$el);
- this.recoverNoteContent(noteText);
- callback();
- });
+ Flash(
+ 'Something went wrong while deleting your note. Please try again.',
+ );
+ this.isDeleting = false;
});
- },
- formCancelHandler(shouldConfirm, isDirty) {
- if (shouldConfirm && isDirty) {
- // eslint-disable-next-line no-alert
- if (!confirm('Are you sure you want to cancel editing this comment?')) return;
- }
- this.$refs.noteBody.resetAutoSave();
- if (this.oldContent) {
- this.note.note_html = this.oldContent;
+ }
+ },
+ formUpdateHandler(noteText, parentElement, callback) {
+ const data = {
+ endpoint: this.note.path,
+ note: {
+ target_type: this.noteableType,
+ target_id: this.note.noteable_id,
+ note: { note: noteText },
+ },
+ };
+ this.isRequesting = true;
+ this.oldContent = this.note.note_html;
+ this.note.note_html = escape(noteText);
+
+ this.updateNote(data)
+ .then(() => {
+ this.isEditing = false;
+ this.isRequesting = false;
this.oldContent = null;
- }
- this.isEditing = false;
- },
- recoverNoteContent(noteText) {
- // we need to do this to prevent noteForm inconsistent content warning
- // this is something we intentionally do so we need to recover the content
- this.note.note = noteText;
- this.$refs.noteBody.$refs.noteForm.note.note = noteText;
- },
+ $(this.$refs.noteBody.$el).renderGFM();
+ this.$refs.noteBody.resetAutoSave();
+ callback();
+ })
+ .catch(() => {
+ this.isRequesting = false;
+ this.isEditing = true;
+ this.$nextTick(() => {
+ const msg =
+ 'Something went wrong while editing your comment. Please try again.';
+ Flash(msg, 'alert', this.$el);
+ this.recoverNoteContent(noteText);
+ callback();
+ });
+ });
+ },
+ formCancelHandler(shouldConfirm, isDirty) {
+ if (shouldConfirm && isDirty) {
+ // eslint-disable-next-line no-alert
+ if (!confirm('Are you sure you want to cancel editing this comment?'))
+ return;
+ }
+ this.$refs.noteBody.resetAutoSave();
+ if (this.oldContent) {
+ this.note.note_html = this.oldContent;
+ this.oldContent = null;
+ }
+ this.isEditing = false;
+ },
+ recoverNoteContent(noteText) {
+ // we need to do this to prevent noteForm inconsistent content warning
+ // this is something we intentionally do so we need to recover the content
+ this.note.note = noteText;
+ this.$refs.noteBody.$refs.noteForm.note.note = noteText;
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index c97472c879c..a90c6d6381d 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,160 +1,162 @@
<script>
- import $ from 'jquery';
- import { mapGetters, mapActions } from 'vuex';
- import { getLocationHash } from '../../lib/utils/url_utility';
- import Flash from '../../flash';
- import store from '../stores/';
- import * as constants from '../constants';
- import noteableNote from './noteable_note.vue';
- import noteableDiscussion from './noteable_discussion.vue';
- import systemNote from '../../vue_shared/components/notes/system_note.vue';
- import commentForm from './comment_form.vue';
- import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
- import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
- import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
+import $ from 'jquery';
+import { mapGetters, mapActions } from 'vuex';
+import { getLocationHash } from '../../lib/utils/url_utility';
+import Flash from '../../flash';
+import store from '../stores/';
+import * as constants from '../constants';
+import noteableNote from './noteable_note.vue';
+import noteableDiscussion from './noteable_discussion.vue';
+import systemNote from '../../vue_shared/components/notes/system_note.vue';
+import commentForm from './comment_form.vue';
+import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
+import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
- export default {
- name: 'NotesApp',
- components: {
- noteableNote,
- noteableDiscussion,
- systemNote,
- commentForm,
- loadingIcon,
- placeholderNote,
- placeholderSystemNote,
+export default {
+ name: 'NotesApp',
+ components: {
+ noteableNote,
+ noteableDiscussion,
+ systemNote,
+ commentForm,
+ loadingIcon,
+ placeholderNote,
+ placeholderSystemNote,
+ },
+ props: {
+ noteableData: {
+ type: Object,
+ required: true,
},
- props: {
- noteableData: {
- type: Object,
- required: true,
- },
- notesData: {
- type: Object,
- required: true,
- },
- userData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
+ notesData: {
+ type: Object,
+ required: true,
},
- store,
- data() {
- return {
- isLoading: true,
- };
+ userData: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- computed: {
- ...mapGetters([
- 'notes',
- 'getNotesDataByProp',
- 'discussionCount',
- ]),
- noteableType() {
- // FIXME -- @fatihacet Get this from JSON data.
- const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
+ },
+ store,
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
+ computed: {
+ ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
+ noteableType() {
+ // FIXME -- @fatihacet Get this from JSON data.
+ const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
- return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
- },
- allNotes() {
- if (this.isLoading) {
- const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
-
- return new Array(totalNotes).fill({
- isSkeletonNote: true,
- });
- }
- return this.notes;
- },
- },
- created() {
- this.setNotesData(this.notesData);
- this.setNoteableData(this.noteableData);
- this.setUserData(this.userData);
+ return this.noteableData.merge_params
+ ? MERGE_REQUEST_NOTEABLE_TYPE
+ : ISSUE_NOTEABLE_TYPE;
},
- mounted() {
- this.fetchNotes();
+ allNotes() {
+ if (this.isLoading) {
+ const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
- const parentElement = this.$el.parentElement;
-
- if (parentElement &&
- parentElement.classList.contains('js-vue-notes-event')) {
- parentElement.addEventListener('toggleAward', (event) => {
- const { awardName, noteId } = event.detail;
- this.actionToggleAward({ awardName, noteId });
+ return new Array(totalNotes).fill({
+ isSkeletonNote: true,
});
}
- document.addEventListener('refreshVueNotes', this.fetchNotes);
- },
- beforeDestroy() {
- document.removeEventListener('refreshVueNotes', this.fetchNotes);
+ return this.notes;
},
- methods: {
- ...mapActions({
- actionFetchNotes: 'fetchNotes',
- poll: 'poll',
- actionToggleAward: 'toggleAward',
- scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
- setNotesData: 'setNotesData',
- setNoteableData: 'setNoteableData',
- setUserData: 'setUserData',
- setLastFetchedAt: 'setLastFetchedAt',
- setTargetNoteHash: 'setTargetNoteHash',
- }),
- getComponentName(note) {
- if (note.isSkeletonNote) {
- return skeletonLoadingContainer;
- }
- if (note.isPlaceholderNote) {
- if (note.placeholderType === constants.SYSTEM_NOTE) {
- return placeholderSystemNote;
- }
- return placeholderNote;
- } else if (note.individual_note) {
- return note.notes[0].system ? systemNote : noteableNote;
- }
+ },
+ created() {
+ this.setNotesData(this.notesData);
+ this.setNoteableData(this.noteableData);
+ this.setUserData(this.userData);
+ },
+ mounted() {
+ this.fetchNotes();
+
+ const parentElement = this.$el.parentElement;
- return noteableDiscussion;
- },
- getComponentData(note) {
- return note.individual_note ? note.notes[0] : note;
- },
- fetchNotes() {
- return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath'))
- .then(() => this.initPolling())
- .then(() => {
- this.isLoading = false;
- })
- .then(() => this.$nextTick())
- .then(() => this.checkLocationHash())
- .catch(() => {
- this.isLoading = false;
- Flash('Something went wrong while fetching comments. Please try again.');
- });
- },
- initPolling() {
- if (this.isPollingInitialized) {
- return;
+ if (
+ parentElement &&
+ parentElement.classList.contains('js-vue-notes-event')
+ ) {
+ parentElement.addEventListener('toggleAward', event => {
+ const { awardName, noteId } = event.detail;
+ this.actionToggleAward({ awardName, noteId });
+ });
+ }
+ document.addEventListener('refreshVueNotes', this.fetchNotes);
+ },
+ beforeDestroy() {
+ document.removeEventListener('refreshVueNotes', this.fetchNotes);
+ },
+ methods: {
+ ...mapActions({
+ actionFetchNotes: 'fetchNotes',
+ poll: 'poll',
+ actionToggleAward: 'toggleAward',
+ scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
+ setNotesData: 'setNotesData',
+ setNoteableData: 'setNoteableData',
+ setUserData: 'setUserData',
+ setLastFetchedAt: 'setLastFetchedAt',
+ setTargetNoteHash: 'setTargetNoteHash',
+ }),
+ getComponentName(note) {
+ if (note.isSkeletonNote) {
+ return skeletonLoadingContainer;
+ }
+ if (note.isPlaceholderNote) {
+ if (note.placeholderType === constants.SYSTEM_NOTE) {
+ return placeholderSystemNote;
}
+ return placeholderNote;
+ } else if (note.individual_note) {
+ return note.notes[0].system ? systemNote : noteableNote;
+ }
- this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
+ return noteableDiscussion;
+ },
+ getComponentData(note) {
+ return note.individual_note ? note.notes[0] : note;
+ },
+ fetchNotes() {
+ return this.actionFetchNotes(this.getNotesDataByProp('discussionsPath'))
+ .then(() => this.initPolling())
+ .then(() => {
+ this.isLoading = false;
+ })
+ .then(() => this.$nextTick())
+ .then(() => this.checkLocationHash())
+ .catch(() => {
+ this.isLoading = false;
+ Flash(
+ 'Something went wrong while fetching comments. Please try again.',
+ );
+ });
+ },
+ initPolling() {
+ if (this.isPollingInitialized) {
+ return;
+ }
- this.poll();
- this.isPollingInitialized = true;
- },
- checkLocationHash() {
- const hash = getLocationHash();
- const element = document.getElementById(hash);
+ this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
- if (hash && element) {
- this.setTargetNoteHash(hash);
- this.scrollToNoteIfNeeded($(element));
- }
- },
+ this.poll();
+ this.isPollingInitialized = true;
+ },
+ checkLocationHash() {
+ const hash = getLocationHash();
+ const element = document.getElementById(hash);
+
+ if (hash && element) {
+ this.setTargetNoteHash(hash);
+ this.scrollToNoteIfNeeded($(element));
+ }
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 545bf2c99a7..f90775d0157 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,35 +1,43 @@
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#js-vue-notes',
- components: {
- notesApp,
- },
- data() {
- const notesDataset = document.getElementById('js-vue-notes').dataset;
- const parsedUserData = JSON.parse(notesDataset.currentUserData);
- const currentUserData = parsedUserData ? {
- id: parsedUserData.id,
- name: parsedUserData.name,
- username: parsedUserData.username,
- avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
- path: parsedUserData.path,
- } : {};
+document.addEventListener(
+ 'DOMContentLoaded',
+ () =>
+ new Vue({
+ el: '#js-vue-notes',
+ components: {
+ notesApp,
+ },
+ data() {
+ const notesDataset = document.getElementById('js-vue-notes').dataset;
+ const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ let currentUserData = {};
+
+ if (parsedUserData) {
+ currentUserData = {
+ id: parsedUserData.id,
+ name: parsedUserData.name,
+ username: parsedUserData.username,
+ avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
+ path: parsedUserData.path,
+ };
+ }
- return {
- noteableData: JSON.parse(notesDataset.noteableData),
- currentUserData,
- notesData: JSON.parse(notesDataset.notesData),
- };
- },
- render(createElement) {
- return createElement('notes-app', {
- props: {
- noteableData: this.noteableData,
- notesData: this.notesData,
- userData: this.currentUserData,
+ return {
+ noteableData: JSON.parse(notesDataset.noteableData),
+ currentUserData,
+ notesData: JSON.parse(notesDataset.notesData),
+ };
+ },
+ render(createElement) {
+ return createElement('notes-app', {
+ props: {
+ noteableData: this.noteableData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ },
+ });
},
- });
- },
-}));
+ }),
+);
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 837a4029346..3dff715905f 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -5,7 +5,11 @@ import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
initAutoSave(noteableType) {
- this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]);
+ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), [
+ 'Note',
+ capitalizeFirstCharacter(noteableType),
+ this.note.id,
+ ]);
},
resetAutoSave() {
this.autosave.reset();
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index ab1ae115e52..f79049b85f6 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -12,7 +12,8 @@ export default {
discussionResolved() {
const { notes, resolved } = this.note;
- if (notes) { // Decide resolved state using store. Only valid for discussions.
+ if (notes) {
+ // Decide resolved state using store. Only valid for discussions.
return notes.every(note => note.resolved && !note.system);
}
@@ -26,7 +27,9 @@ export default {
return __('Comment and resolve discussion');
}
- return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
+ return this.discussionResolved
+ ? __('Unresolve discussion')
+ : __('Resolve discussion');
},
},
methods: {
@@ -42,7 +45,9 @@ export default {
})
.catch(() => {
this.isResolving = false;
- const msg = __('Something went wrong while resolving this discussion. Please try again.');
+ const msg = __(
+ 'Something went wrong while resolving this discussion. Please try again.',
+ );
Flash(msg, 'alert', this.$el);
});
},
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index b4c19a9ec22..7c623aac6ed 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -22,7 +22,9 @@ export default {
},
toggleResolveNote(endpoint, isResolved) {
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
- const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
+ const method = isResolved
+ ? UNRESOLVE_NOTE_METHOD_NAME
+ : RESOLVE_NOTE_METHOD_NAME;
return Vue.http[method](endpoint);
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index dc0e3c39775..244a6980b5a 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -12,86 +12,115 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll;
-export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
-export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
-export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
-export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
-export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
-export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
-export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
-
-export const fetchNotes = ({ commit }, path) => service
- .fetchNotes(path)
- .then(res => res.json())
- .then((res) => {
- commit(types.SET_INITIAL_NOTES, res);
- });
+export const setNotesData = ({ commit }, data) =>
+ commit(types.SET_NOTES_DATA, data);
+export const setNoteableData = ({ commit }, data) =>
+ commit(types.SET_NOTEABLE_DATA, data);
+export const setUserData = ({ commit }, data) =>
+ commit(types.SET_USER_DATA, data);
+export const setLastFetchedAt = ({ commit }, data) =>
+ commit(types.SET_LAST_FETCHED_AT, data);
+export const setInitialNotes = ({ commit }, data) =>
+ commit(types.SET_INITIAL_NOTES, data);
+export const setTargetNoteHash = ({ commit }, data) =>
+ commit(types.SET_TARGET_NOTE_HASH, data);
+export const toggleDiscussion = ({ commit }, data) =>
+ commit(types.TOGGLE_DISCUSSION, data);
+
+export const fetchNotes = ({ commit }, path) =>
+ service
+ .fetchNotes(path)
+ .then(res => res.json())
+ .then(res => {
+ commit(types.SET_INITIAL_NOTES, res);
+ });
-export const deleteNote = ({ commit }, note) => service
- .deleteNote(note.path)
- .then(() => {
+export const deleteNote = ({ commit }, note) =>
+ service.deleteNote(note.path).then(() => {
commit(types.DELETE_NOTE, note);
});
-export const updateNote = ({ commit }, { endpoint, note }) => service
- .updateNote(endpoint, note)
- .then(res => res.json())
- .then((res) => {
- commit(types.UPDATE_NOTE, res);
- });
+export const updateNote = ({ commit }, { endpoint, note }) =>
+ service
+ .updateNote(endpoint, note)
+ .then(res => res.json())
+ .then(res => {
+ commit(types.UPDATE_NOTE, res);
+ });
-export const replyToDiscussion = ({ commit }, { endpoint, data }) => service
- .replyToDiscussion(endpoint, data)
- .then(res => res.json())
- .then((res) => {
- commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
+export const replyToDiscussion = ({ commit }, { endpoint, data }) =>
+ service
+ .replyToDiscussion(endpoint, data)
+ .then(res => res.json())
+ .then(res => {
+ commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
- return res;
- });
+ return res;
+ });
-export const createNewNote = ({ commit }, { endpoint, data }) => service
- .createNewNote(endpoint, data)
- .then(res => res.json())
- .then((res) => {
- if (!res.errors) {
- commit(types.ADD_NEW_NOTE, res);
- }
- return res;
- });
+export const createNewNote = ({ commit }, { endpoint, data }) =>
+ service
+ .createNewNote(endpoint, data)
+ .then(res => res.json())
+ .then(res => {
+ if (!res.errors) {
+ commit(types.ADD_NEW_NOTE, res);
+ }
+ return res;
+ });
export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES);
-export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service
- .toggleResolveNote(endpoint, isResolved)
- .then(res => res.json())
- .then((res) => {
- const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
+export const toggleResolveNote = (
+ { commit },
+ { endpoint, isResolved, discussion },
+) =>
+ service
+ .toggleResolveNote(endpoint, isResolved)
+ .then(res => res.json())
+ .then(res => {
+ const mutationType = discussion
+ ? types.UPDATE_DISCUSSION
+ : types.UPDATE_NOTE;
- commit(mutationType, res);
- });
+ commit(mutationType, res);
+ });
-export const closeIssue = ({ commit, dispatch, state }) => service
- .toggleIssueState(state.notesData.closePath)
- .then(res => res.json())
- .then((data) => {
- commit(types.CLOSE_ISSUE);
- dispatch('emitStateChangedEvent', data);
- });
+export const closeIssue = ({ commit, dispatch, state }) => {
+ dispatch('toggleStateButtonLoading', true);
+ return service
+ .toggleIssueState(state.notesData.closePath)
+ .then(res => res.json())
+ .then(data => {
+ commit(types.CLOSE_ISSUE);
+ dispatch('emitStateChangedEvent', data);
+ dispatch('toggleStateButtonLoading', false);
+ });
+};
-export const reopenIssue = ({ commit, dispatch, state }) => service
- .toggleIssueState(state.notesData.reopenPath)
- .then(res => res.json())
- .then((data) => {
- commit(types.REOPEN_ISSUE);
- dispatch('emitStateChangedEvent', data);
- });
+export const reopenIssue = ({ commit, dispatch, state }) => {
+ dispatch('toggleStateButtonLoading', true);
+ return service
+ .toggleIssueState(state.notesData.reopenPath)
+ .then(res => res.json())
+ .then(data => {
+ commit(types.REOPEN_ISSUE);
+ dispatch('emitStateChangedEvent', data);
+ dispatch('toggleStateButtonLoading', false);
+ });
+};
+
+export const toggleStateButtonLoading = ({ commit }, value) =>
+ commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
export const emitStateChangedEvent = ({ commit, getters }, data) => {
- const event = new CustomEvent('issuable_vue_app:change', { detail: {
- data,
- isClosed: getters.openState === constants.CLOSED,
- } });
+ const event = new CustomEvent('issuable_vue_app:change', {
+ detail: {
+ data,
+ isClosed: getters.openState === constants.CLOSED,
+ },
+ });
document.dispatchEvent(event);
};
@@ -133,59 +162,70 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
});
}
- return dispatch(methodToDispatch, noteData)
- .then((res) => {
- const { errors } = res;
- const commandsChanges = res.commands_changes;
+ return dispatch(methodToDispatch, noteData).then(res => {
+ const { errors } = res;
+ const commandsChanges = res.commands_changes;
- if (hasQuickActions && errors && Object.keys(errors).length) {
- eTagPoll.makeRequest();
+ if (hasQuickActions && errors && Object.keys(errors).length) {
+ eTagPoll.makeRequest();
- $('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash('Commands applied', 'notice', noteData.flashContainer);
- }
-
- if (commandsChanges) {
- if (commandsChanges.emoji_award) {
- const votesBlock = $('.js-awards-block').eq(0);
-
- loadAwardsHandler()
- .then((awardsHandler) => {
- awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
- awardsHandler.scrollToAwards();
- })
- .catch(() => {
- Flash(
- 'Something went wrong while adding your award. Please try again.',
- 'alert',
- noteData.flashContainer,
- );
- });
- }
+ $('.js-gfm-input').trigger('clear-commands-cache.atwho');
+ Flash('Commands applied', 'notice', noteData.flashContainer);
+ }
- if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
- sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
- }
+ if (commandsChanges) {
+ if (commandsChanges.emoji_award) {
+ const votesBlock = $('.js-awards-block').eq(0);
+
+ loadAwardsHandler()
+ .then(awardsHandler => {
+ awardsHandler.addAwardToEmojiBar(
+ votesBlock,
+ commandsChanges.emoji_award,
+ );
+ awardsHandler.scrollToAwards();
+ })
+ .catch(() => {
+ Flash(
+ 'Something went wrong while adding your award. Please try again.',
+ 'alert',
+ noteData.flashContainer,
+ );
+ });
}
- if (errors && errors.commands_only) {
- Flash(errors.commands_only, 'notice', noteData.flashContainer);
+ if (
+ commandsChanges.spend_time != null ||
+ commandsChanges.time_estimate != null
+ ) {
+ sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
}
- commit(types.REMOVE_PLACEHOLDER_NOTES);
+ }
- return res;
- });
+ if (errors && errors.commands_only) {
+ Flash(errors.commands_only, 'notice', noteData.flashContainer);
+ }
+ commit(types.REMOVE_PLACEHOLDER_NOTES);
+
+ return res;
+ });
};
const pollSuccessCallBack = (resp, commit, state, getters) => {
if (resp.notes && resp.notes.length) {
const { notesById } = getters;
- resp.notes.forEach((note) => {
+ resp.notes.forEach(note => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
- } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
- const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
+ } else if (
+ note.type === constants.DISCUSSION_NOTE ||
+ note.type === constants.DIFF_NOTE
+ ) {
+ const discussion = utils.findNoteObjectById(
+ state.notes,
+ note.discussion_id,
+ );
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
@@ -208,9 +248,12 @@ export const poll = ({ commit, state, getters }) => {
resource: service,
method: 'poll',
data: state,
- successCallback: resp => resp.json()
- .then(data => pollSuccessCallBack(data, commit, state, getters)),
- errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
+ successCallback: resp =>
+ resp
+ .json()
+ .then(data => pollSuccessCallBack(data, commit, state, getters)),
+ errorCallback: () =>
+ Flash('Something went wrong while fetching latest comments.'),
});
if (!Visibility.hidden()) {
@@ -237,15 +280,22 @@ export const restartPolling = () => {
};
export const fetchData = ({ commit, state, getters }) => {
- const requestData = { endpoint: state.notesData.notesPath, lastFetchedAt: state.lastFetchedAt };
+ const requestData = {
+ endpoint: state.notesData.notesPath,
+ lastFetchedAt: state.lastFetchedAt,
+ };
- service.poll(requestData)
+ service
+ .poll(requestData)
.then(resp => resp.json)
.then(data => pollSuccessCallBack(data, commit, state, getters))
.catch(() => Flash('Something went wrong while fetching latest comments.'));
};
-export const toggleAward = ({ commit, state, getters, dispatch }, { awardName, noteId }) => {
+export const toggleAward = (
+ { commit, state, getters, dispatch },
+ { awardName, noteId },
+) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index e6180101c58..f89591a54d6 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -11,27 +11,31 @@ export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
-export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
+export const getUserDataByProp = state => prop =>
+ state.userData && state.userData[prop];
-export const notesById = state => state.notes.reduce((acc, note) => {
- note.notes.every(n => Object.assign(acc, { [n.id]: n }));
- return acc;
-}, {});
+export const notesById = state =>
+ state.notes.reduce((acc, note) => {
+ note.notes.every(n => Object.assign(acc, { [n.id]: n }));
+ return acc;
+ }, {});
const reverseNotes = array => array.slice(0).reverse();
-const isLastNote = (note, state) => !note.system &&
- state.userData && note.author &&
+const isLastNote = (note, state) =>
+ !note.system &&
+ state.userData &&
+ note.author &&
note.author.id === state.userData.id;
-export const getCurrentUserLastNote = state => _.flatten(
- reverseNotes(state.notes)
- .map(note => reverseNotes(note.notes)),
+export const getCurrentUserLastNote = state =>
+ _.flatten(
+ reverseNotes(state.notes).map(note => reverseNotes(note.notes)),
).find(el => isLastNote(el, state));
-export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes)
- .find(el => isLastNote(el, state));
+export const getDiscussionLastNote = state => discussion =>
+ reverseNotes(discussion.notes).find(el => isLastNote(el, state));
-export const discussionCount = (state) => {
+export const discussionCount = state => {
const discussions = state.notes.filter(n => !n.individual_note);
return discussions.length;
@@ -43,10 +47,10 @@ export const unresolvedDiscussions = (state, getters) => {
return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
};
-export const resolvedDiscussionsById = (state) => {
+export const resolvedDiscussionsById = state => {
const map = {};
- state.notes.forEach((n) => {
+ state.notes.forEach(n => {
if (n.notes) {
const resolved = n.notes.every(note => note.resolved && !note.system);
diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js
index 488a9ca38d3..9ed19bf171e 100644
--- a/app/assets/javascripts/notes/stores/index.js
+++ b/app/assets/javascripts/notes/stores/index.js
@@ -12,6 +12,9 @@ export default new Vuex.Store({
targetNoteHash: null,
lastFetchedAt: null,
+ // View layer
+ isToggleStateButtonLoading: false,
+
// holds endpoints and permissions provided through haml
notesData: {},
userData: {},
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index da1b5a9e51a..b455e23ecde 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -17,3 +17,4 @@ export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
export const REOPEN_ISSUE = 'REOPEN_ISSUE';
+export const TOGGLE_STATE_BUTTON_LOADING = 'TOGGLE_STATE_BUTTON_LOADING';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 949628a65c0..c8edc06349f 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -7,7 +7,7 @@ export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id);
- const isDiscussion = (type === constants.DISCUSSION_NOTE);
+ const isDiscussion = type === constants.DISCUSSION_NOTE;
if (!exists) {
const noteData = {
@@ -63,13 +63,15 @@ export default {
const note = notes[i];
const children = note.notes;
- if (children.length && !note.individual_note) { // remove placeholder from discussions
+ if (children.length && !note.individual_note) {
+ // remove placeholder from discussions
for (let j = children.length - 1; j >= 0; j -= 1) {
if (children[j].isPlaceholderNote) {
children.splice(j, 1);
}
}
- } else if (note.isPlaceholderNote) { // remove placeholders from state root
+ } else if (note.isPlaceholderNote) {
+ // remove placeholders from state root
notes.splice(i, 1);
}
}
@@ -89,10 +91,10 @@ export default {
[types.SET_INITIAL_NOTES](state, notesData) {
const notes = [];
- notesData.forEach((note) => {
+ notesData.forEach(note => {
// To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) {
- note.notes.forEach((n) => {
+ note.notes.forEach(n => {
notes.push({
...note,
notes: [n], // override notes array to only have one item to mimick individual_note
@@ -103,7 +105,7 @@ export default {
notes.push({
...note,
- expanded: (oldNote ? oldNote.expanded : note.expanded),
+ expanded: oldNote ? oldNote.expanded : note.expanded,
});
}
});
@@ -128,7 +130,9 @@ export default {
notesArr.push({
individual_note: true,
isPlaceholderNote: true,
- placeholderType: data.isSystemNote ? constants.SYSTEM_NOTE : constants.NOTE,
+ placeholderType: data.isSystemNote
+ ? constants.SYSTEM_NOTE
+ : constants.NOTE,
notes: [
{
body: data.noteBody,
@@ -141,12 +145,16 @@ export default {
const { awardName, note } = data;
const { id, name, username } = state.userData;
- const hasEmojiAwardedByCurrentUser = note.award_emoji
- .filter(emoji => emoji.name === data.awardName && emoji.user.id === id);
+ const hasEmojiAwardedByCurrentUser = note.award_emoji.filter(
+ emoji => emoji.name === data.awardName && emoji.user.id === id,
+ );
if (hasEmojiAwardedByCurrentUser.length) {
// If current user has awarded this emoji, remove it.
- note.award_emoji.splice(note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]), 1);
+ note.award_emoji.splice(
+ note.award_emoji.indexOf(hasEmojiAwardedByCurrentUser[0]),
+ 1,
+ );
} else {
note.award_emoji.push({
name: awardName,
@@ -199,4 +207,8 @@ export default {
[types.REOPEN_ISSUE](state) {
Object.assign(state.noteableData, { state: constants.REOPENED });
},
+
+ [types.TOGGLE_STATE_BUTTON_LOADING](state, value) {
+ Object.assign(state, { isToggleStateButtonLoading: value });
+ },
};
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 275263a2aaa..a0e096ebfaf 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -2,13 +2,15 @@ import AjaxCache from '~/lib/utils/ajax_cache';
const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
-export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
+export const findNoteObjectById = (notes, id) =>
+ notes.filter(n => n.id === id)[0];
-export const getQuickActionText = (note) => {
+export const getQuickActionText = note => {
let text = 'Applying command';
- const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
+ const quickActions =
+ AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || [];
- const executedCommands = quickActions.filter((command) => {
+ const executedCommands = quickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(note);
});
@@ -27,4 +29,5 @@ export const getQuickActionText = (note) => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
-export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
+export const stripQuickActions = note =>
+ note.replace(REGEX_QUICK_ACTIONS, '').trim();
diff --git a/app/assets/javascripts/pages/ci/lints/create/index.js b/app/assets/javascripts/pages/ci/lints/new/index.js
index 8e8a843da0b..8e8a843da0b 100644
--- a/app/assets/javascripts/pages/ci/lints/create/index.js
+++ b/app/assets/javascripts/pages/ci/lints/new/index.js
diff --git a/app/assets/javascripts/pages/profiles/notifications/show/index.js b/app/assets/javascripts/pages/profiles/notifications/show/index.js
new file mode 100644
index 00000000000..2e24a10fa5c
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/notifications/show/index.js
@@ -0,0 +1,7 @@
+import NotificationsForm from '../../../../notifications_form';
+import notificationsDropdown from '../../../../notifications_dropdown';
+
+document.addEventListener('DOMContentLoaded', () => {
+ new NotificationsForm(); // eslint-disable-line no-new
+ notificationsDropdown();
+});
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 26cbb279d4a..85c6862d629 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -1,7 +1,29 @@
+import Vue from 'vue';
+import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import BlobViewer from '~/blob/viewer/index';
import initBlob from '~/pages/projects/init_blob';
document.addEventListener('DOMContentLoaded', () => {
new BlobViewer(); // eslint-disable-line no-new
initBlob();
+
+ const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
+ const statusLink = document.querySelector('.commit-actions .ci-status-link');
+ if (statusLink) {
+ statusLink.remove();
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: CommitPipelineStatusEl,
+ components: {
+ commitPipelineStatus,
+ },
+ render(createElement) {
+ return createElement('commit-pipeline-status', {
+ props: {
+ endpoint: CommitPipelineStatusEl.dataset.endpoint,
+ },
+ });
+ },
+ });
+ }
});
diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js
index ef44e2323ef..c22598ee665 100644
--- a/app/assets/javascripts/performance_bar.js
+++ b/app/assets/javascripts/performance_bar.js
@@ -14,8 +14,6 @@ export default class PerformanceBar {
init(opts) {
const $container = $(opts.container);
- this.$sqlProfileLink = $container.find('.js-toggle-modal-peek-sql');
- this.$sqlProfileModal = $container.find('#modal-peek-pg-queries');
this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile');
this.$lineProfileModal = $('#modal-peek-line-profile');
this.initEventListeners();
@@ -23,7 +21,6 @@ export default class PerformanceBar {
}
initEventListeners() {
- this.$sqlProfileLink.on('click', () => this.handleSQLProfileLink());
this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e));
$(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile);
}
@@ -36,10 +33,6 @@ export default class PerformanceBar {
}
}
- handleSQLProfileLink() {
- PerformanceBar.toggleModal(this.$sqlProfileModal);
- }
-
handleLineProfileLink(e) {
const lineProfilerParameter = getParameterValues('lineprofiler');
const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
new file mode 100644
index 00000000000..7bef2e97349
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -0,0 +1,144 @@
+<script>
+import timeagoMixin from '../../vue_shared/mixins/timeago';
+import tooltip from '../../vue_shared/directives/tooltip';
+import LoadingButton from '../../vue_shared/components/loading_button.vue';
+import { visitUrl } from '../../lib/utils/url_utility';
+import createFlash from '../../flash';
+import MemoryUsage from './memory_usage.vue';
+import StatusIcon from './mr_widget_status_icon.vue';
+import MRWidgetService from '../services/mr_widget_service';
+
+export default {
+ name: 'Deployment',
+ components: {
+ LoadingButton,
+ MemoryUsage,
+ StatusIcon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [
+ timeagoMixin,
+ ],
+ props: {
+ deployment: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isStopping: false,
+ };
+ },
+ computed: {
+ deployTimeago() {
+ return this.timeFormated(this.deployment.deployed_at);
+ },
+ hasExternalUrls() {
+ return !!(this.deployment.external_url && this.deployment.external_url_formatted);
+ },
+ hasDeploymentTime() {
+ return !!(this.deployment.deployed_at && this.deployment.deployed_at_formatted);
+ },
+ hasDeploymentMeta() {
+ return !!(this.deployment.url && this.deployment.name);
+ },
+ hasMetrics() {
+ return !!(this.deployment.metrics_url);
+ },
+ },
+ methods: {
+ stopEnvironment() {
+ const msg = 'Are you sure you want to stop this environment?';
+ const isConfirmed = confirm(msg); // eslint-disable-line
+
+ if (isConfirmed) {
+ this.isStopping = true;
+
+ MRWidgetService.stopEnvironment(this.deployment.stop_url)
+ .then(res => res.data)
+ .then((data) => {
+ if (data.redirect_url) {
+ visitUrl(data.redirect_url);
+ }
+
+ this.isStopping = false;
+ })
+ .catch(() => {
+ createFlash('Something went wrong while stopping this environment. Please try again.');
+ this.isStopping = false;
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-heading deploy-heading">
+ <div class="ci-widget media">
+ <div class="ci-status-icon ci-status-icon-success">
+ <span class="js-icon-link icon-link">
+ <status-icon status="success" />
+ </span>
+ </div>
+ <div class="media-body">
+ <div class="deploy-body">
+ <template v-if="hasDeploymentMeta">
+ <span>
+ Deployed to
+ </span>
+ <a
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="deploy-link js-deploy-meta"
+ >
+ {{ deployment.name }}
+ </a>
+ </template>
+ <template v-if="hasExternalUrls">
+ <span>
+ on
+ </span>
+ <a
+ :href="deployment.external_url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="deploy-link js-deploy-url"
+ >
+ <i
+ class="fa fa-external-link"
+ aria-hidden="true"
+ >
+ </i>
+ {{ deployment.external_url_formatted }}
+ </a>
+ </template>
+ <span
+ v-if="hasDeploymentTime"
+ v-tooltip
+ :title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
+ >
+ {{ deployTimeago }}
+ </span>
+ <loading-button
+ v-if="deployment.stop_url"
+ container-class="btn btn-default btn-xs prepend-left-default"
+ label="Stop environment"
+ :loading="isStopping"
+ @click="stopEnvironment"
+ />
+ </div>
+ <memory-usage
+ v-if="hasMetrics"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
deleted file mode 100644
index c7f992384c8..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ /dev/null
@@ -1,113 +0,0 @@
-import { getTimeago } from '~/lib/utils/datetime_utility';
-import { visitUrl } from '../../lib/utils/url_utility';
-import Flash from '../../flash';
-import MemoryUsage from './memory_usage.vue';
-import StatusIcon from './mr_widget_status_icon.vue';
-import MRWidgetService from '../services/mr_widget_service';
-
-export default {
- name: 'MRWidgetDeployment',
- props: {
- mr: { type: Object, required: true },
- service: { type: Object, required: true },
- },
- components: {
- MemoryUsage,
- StatusIcon,
- },
- methods: {
- formatDate(date) {
- return getTimeago().format(date);
- },
- hasExternalUrls(deployment = {}) {
- return deployment.external_url && deployment.external_url_formatted;
- },
- hasDeploymentTime(deployment = {}) {
- return deployment.deployed_at && deployment.deployed_at_formatted;
- },
- hasDeploymentMeta(deployment = {}) {
- return deployment.url && deployment.name;
- },
- stopEnvironment(deployment) {
- const msg = 'Are you sure you want to stop this environment?';
- const isConfirmed = confirm(msg); // eslint-disable-line
-
- if (isConfirmed) {
- MRWidgetService.stopEnvironment(deployment.stop_url)
- .then(res => res.data)
- .then((data) => {
- if (data.redirect_url) {
- visitUrl(data.redirect_url);
- }
- })
- .catch(() => {
- new Flash('Something went wrong while stopping this environment. Please try again.'); // eslint-disable-line
- });
- }
- },
- },
- template: `
- <div class="mr-widget-heading deploy-heading">
- <div v-for="deployment in mr.deployments">
- <div class="ci-widget media">
- <div class="ci-status-icon ci-status-icon-success">
- <span class="js-icon-link icon-link">
- <status-icon status="success" />
- </span>
- </div>
- <div class="media-body space-children">
- <span>
- <span
- v-if="hasDeploymentMeta(deployment)">
- Deployed to
- </span>
- <a
- v-if="hasDeploymentMeta(deployment)"
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-meta inline">
- {{deployment.name}}
- </a>
- <span
- v-if="hasExternalUrls(deployment)">
- on
- </span>
- <a
- v-if="hasExternalUrls(deployment)"
- :href="deployment.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-url inline">
- <i
- class="fa fa-external-link"
- aria-hidden="true" />
- {{deployment.external_url_formatted}}
- </a>
- <span
- v-if="hasDeploymentTime(deployment)"
- :data-title="deployment.deployed_at_formatted"
- class="js-deploy-time"
- data-toggle="tooltip"
- data-placement="top">
- {{formatDate(deployment.deployed_at)}}
- </span>
- </span>
- <button
- type="button"
- v-if="deployment.stop_url"
- @click="stopEnvironment(deployment)"
- class="btn btn-default btn-xs">
- Stop environment
- </button>
- <memory-usage
- v-if="deployment.metrics_url"
- :metrics-url="deployment.metrics_url"
- :metrics-monitoring-url="deployment.metrics_monitoring_url"
- />
- </div>
- </div>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 20624aad0ad..efbe1c96d1c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -14,7 +14,7 @@ export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header.vue';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help.vue';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
-export { default as WidgetDeployment } from './components/mr_widget_deployment';
+export { default as Deployment } from './components/deployment.vue';
export { default as WidgetMaintainerEdit } from './components/mr_widget_maintainer_edit.vue';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links.vue';
export { default as MergedState } from './components/states/mr_widget_merged.vue';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index cc8bc6af1e1..169adfe0a1d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -5,7 +5,7 @@ import {
WidgetHeader,
WidgetMergeHelp,
WidgetPipeline,
- WidgetDeployment,
+ Deployment,
WidgetMaintainerEdit,
WidgetRelatedLinks,
MergedState,
@@ -67,9 +67,6 @@ export default {
shouldRenderRelatedLinks() {
return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
},
- shouldRenderDeployments() {
- return this.mr.deployments.length;
- },
shouldRenderSourceBranchRemovalStatus() {
return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch &&
(!this.mr.isNothingToMergeState && !this.mr.isMergedState);
@@ -216,7 +213,7 @@ export default {
'mr-widget-header': WidgetHeader,
'mr-widget-merge-help': WidgetMergeHelp,
'mr-widget-pipeline': WidgetPipeline,
- 'mr-widget-deployment': WidgetDeployment,
+ Deployment,
'mr-widget-maintainer-edit': WidgetMaintainerEdit,
'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState,
@@ -250,10 +247,11 @@ export default {
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
/>
- <mr-widget-deployment
- v-if="shouldRenderDeployments"
- :mr="mr"
- :service="service" />
+ <deployment
+ v-for="deployment in mr.deployments"
+ :key="deployment.id"
+ :deployment="deployment"
+ />
<div class="mr-widget-section">
<component
:is="componentName"
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index ddd9dbb2be4..e12b5aab381 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -17,8 +17,6 @@
*/
@mixin markdown-table {
width: auto;
- display: block;
- overflow-x: auto;
}
/*
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 8b680c2dc52..b487f6278c2 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -194,8 +194,6 @@
.commit-actions {
@media (min-width: $screen-sm-min) {
- font-size: 0;
-
.fa-spinner {
font-size: 12px;
}
@@ -204,7 +202,7 @@
.ci-status-link {
display: inline-block;
position: relative;
- top: 1px;
+ top: 2px;
}
.btn-clipboard,
@@ -226,7 +224,7 @@
.ci-status-icon {
position: relative;
- top: 1px;
+ top: 2px;
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index f887a11004f..4692d0fb873 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -718,6 +718,8 @@
}
.mr-memory-usage {
+ width: 100%;
+
p.usage-info-loading .usage-info-load-spinner {
margin-right: 10px;
font-size: 16px;
@@ -727,3 +729,36 @@
.fork-sprite {
margin-right: -5px;
}
+
+.deploy-heading {
+ .media-body {
+ min-width: 0;
+ }
+}
+
+.deploy-body {
+ display: flex;
+ flex-wrap: wrap;
+
+ @media (min-width: $screen-xs) {
+ flex-wrap: nowrap;
+ white-space: nowrap;
+ }
+
+ > *:not(:last-child) {
+ margin-right: .3em;
+ }
+}
+
+.deploy-link {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 100px;
+ max-width: 150px;
+
+ @media (min-width: $screen-xs) {
+ min-width: 0;
+ max-width: 100%;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 3c565837383..085a2e74328 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -16,7 +16,7 @@ ul.notes {
.note-created-ago,
.note-updated-at {
- white-space: nowrap;
+ white-space: normal;
}
.discussion-body {
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index e70a57c2a67..9a0ec936979 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -180,6 +180,11 @@ ul.wiki-pages-list.content-list {
}
}
+.wiki-holder {
+ overflow-x: auto;
+ overflow-y: hidden;
+}
+
.wiki {
table {
@include markdown-table;
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index ee507009e50..cba9a53dc4b 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -19,6 +19,12 @@ class Projects::DiscussionsController < Projects::ApplicationController
render_discussion
end
+ def show
+ render json: {
+ discussion_html: view_to_html_string('discussions/_diff_with_notes', discussion: discussion, expanded: true)
+ }
+ end
+
private
def render_discussion
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
index d6bcd939522..5c507fe8d50 100644
--- a/app/finders/admin/projects_finder.rb
+++ b/app/finders/admin/projects_finder.rb
@@ -16,8 +16,7 @@ class Admin::ProjectsFinder
items = by_archived(items)
items = by_personal(items)
items = by_name(items)
- items = sort(items)
- items.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
+ sort(items).page(params[:page])
end
private
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index d5e77c7e271..cd4075b340d 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -2,9 +2,4 @@ module JavascriptHelper
def page_specific_javascript_tag(js)
javascript_include_tag asset_path(js)
end
-
- # deprecated; use webpack_bundle_tag directly instead
- def page_specific_javascript_bundle_tag(bundle)
- webpack_bundle_tag(bundle)
- end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 0dee6df525d..3cbbf8b5dfa 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -347,15 +347,15 @@ class ApplicationSetting < ActiveRecord::Base
end
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
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
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
def domain_whitelist_raw
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b230b7f47ef..f8a3600e863 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -252,23 +252,23 @@ module Ci
# All variables, including those dependent on environment, which could
# contain unexpanded variables.
def variables(environment: persisted_environment)
- variables = predefined_variables
- variables += project.predefined_variables
- variables += pipeline.predefined_variables
- variables += runner.predefined_variables if runner
- variables += project.container_registry_variables
- variables += project.deployment_variables if has_environment?
- variables += project.auto_devops_variables
- variables += yaml_variables
- variables += user_variables
- variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
- variables += secret_variables(environment: environment)
- variables += trigger_request.user_variables if trigger_request
- variables += pipeline.variables.map(&:to_runner_variable)
- variables += pipeline.pipeline_schedule.job_variables if pipeline.pipeline_schedule
- variables += persisted_environment_variables if environment
-
- variables
+ collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.concat(predefined_variables)
+ variables.concat(project.predefined_variables)
+ variables.concat(pipeline.predefined_variables)
+ variables.concat(runner.predefined_variables) if runner
+ variables.concat(project.deployment_variables(environment: environment)) if has_environment?
+ variables.concat(yaml_variables)
+ variables.concat(user_variables)
+ variables.concat(project.group.secret_variables_for(ref, project)) if project.group
+ variables.concat(secret_variables(environment: environment))
+ variables.concat(trigger_request.user_variables) if trigger_request
+ variables.concat(pipeline.variables)
+ variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
+ variables.concat(persisted_environment_variables) if environment
+ end
+
+ collection.to_runner_variables
end
def features
@@ -430,14 +430,14 @@ module Ci
end
def user_variables
- return [] if user.blank?
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables if user.blank?
- [
- { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
- { key: 'GITLAB_USER_EMAIL', value: user.email, public: true },
- { key: 'GITLAB_USER_LOGIN', value: user.username, public: true },
- { key: 'GITLAB_USER_NAME', value: user.name, public: true }
- ]
+ variables.append(key: 'GITLAB_USER_ID', value: user.id.to_s)
+ variables.append(key: 'GITLAB_USER_EMAIL', value: user.email)
+ variables.append(key: 'GITLAB_USER_LOGIN', value: user.username)
+ variables.append(key: 'GITLAB_USER_NAME', value: user.name)
+ end
end
def secret_variables(environment: persisted_environment)
@@ -540,60 +540,57 @@ module Ci
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
def predefined_variables
- variables = [
- { key: 'CI', value: 'true', public: true },
- { key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true },
- { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
- { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
- { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
- { key: 'CI_JOB_ID', value: id.to_s, public: true },
- { key: 'CI_JOB_NAME', value: name, public: true },
- { key: 'CI_JOB_STAGE', value: stage, public: true },
- { key: 'CI_JOB_TOKEN', value: token, public: false },
- { key: 'CI_COMMIT_SHA', value: sha, public: true },
- { key: 'CI_COMMIT_REF_NAME', value: ref, public: true },
- { key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true },
- { key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true },
- { key: 'CI_REGISTRY_PASSWORD', value: token, public: false },
- { key: 'CI_REPOSITORY_URL', value: repo_url, public: false }
- ]
-
- variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag?
- variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request
- variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action?
- variables.concat(legacy_variables)
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI', value: 'true')
+ variables.append(key: 'GITLAB_CI', value: 'true')
+ variables.append(key: 'GITLAB_FEATURES', value: project.namespace.features.join(','))
+ variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
+ variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
+ variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
+ variables.append(key: 'CI_JOB_ID', value: id.to_s)
+ variables.append(key: 'CI_JOB_NAME', value: name)
+ variables.append(key: 'CI_JOB_STAGE', value: stage)
+ variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
+ variables.append(key: 'CI_COMMIT_SHA', value: sha)
+ variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
+ variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
+ variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
+ variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
+ variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
+ variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
+ variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
+ variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
+ variables.concat(legacy_variables)
+ end
end
def persisted_environment_variables
- return [] unless persisted_environment
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables unless persisted_environment
- variables = persisted_environment.predefined_variables
+ variables.concat(persisted_environment.predefined_variables)
- # Here we're passing unexpanded environment_url for runner to expand,
- # and we need to make sure that CI_ENVIRONMENT_NAME and
- # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
- variables << { key: 'CI_ENVIRONMENT_URL', value: environment_url, public: true } if environment_url
-
- variables
+ # Here we're passing unexpanded environment_url for runner to expand,
+ # and we need to make sure that CI_ENVIRONMENT_NAME and
+ # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
+ variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
+ end
end
def legacy_variables
- variables = [
- { key: 'CI_BUILD_ID', value: id.to_s, public: true },
- { key: 'CI_BUILD_TOKEN', value: token, public: false },
- { key: 'CI_BUILD_REF', value: sha, public: true },
- { key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true },
- { key: 'CI_BUILD_REF_NAME', value: ref, public: true },
- { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
- { key: 'CI_BUILD_NAME', value: name, public: true },
- { key: 'CI_BUILD_STAGE', value: stage, public: true }
- ]
-
- variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag?
- variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request
- variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action?
- variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables.append(key: 'CI_BUILD_ID', value: id.to_s)
+ variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
+ variables.append(key: 'CI_BUILD_REF', value: sha)
+ variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
+ variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
+ variables.append(key: 'CI_BUILD_REF_SLUG', value: ref_slug)
+ variables.append(key: 'CI_BUILD_NAME', value: name)
+ variables.append(key: 'CI_BUILD_STAGE', value: stage)
+ variables.append(key: "CI_BUILD_TAG", value: ref) if tag?
+ variables.append(key: "CI_BUILD_TRIGGERED", value: 'true') if trigger_request
+ variables.append(key: "CI_BUILD_MANUAL", value: 'true') if action?
+ end
end
def environment_url
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index a72a815bfe8..4966ea62df9 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -473,11 +473,10 @@ module Ci
end
def predefined_variables
- [
- { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
- { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true },
- { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
- ]
+ Gitlab::Ci::Variables::Collection.new
+ .append(key: 'CI_PIPELINE_ID', value: id.to_s)
+ .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
+ .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
end
def queued_duration
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 609620a62bb..7173f88f1c7 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -132,11 +132,10 @@ module Ci
end
def predefined_variables
- [
- { key: 'CI_RUNNER_ID', value: id.to_s, public: true },
- { key: 'CI_RUNNER_DESCRIPTION', value: description, public: true },
- { key: 'CI_RUNNER_TAGS', value: tag_list.to_s, public: true }
- ]
+ Gitlab::Ci::Variables::Collection.new
+ .append(key: 'CI_RUNNER_ID', value: id.to_s)
+ .append(key: 'CI_RUNNER_DESCRIPTION', value: description)
+ .append(key: 'CI_RUNNER_TAGS', value: tag_list.to_s)
end
def tick_runner_queue
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 738d1e2bc82..ba6552f238f 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -56,19 +56,19 @@ module Clusters
def predefined_variables
config = YAML.dump(kubeconfig)
- variables = [
- { key: 'KUBE_URL', value: api_url, public: true },
- { key: 'KUBE_TOKEN', value: token, public: false },
- { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true },
- { key: 'KUBECONFIG', value: config, public: false, file: true }
- ]
-
- if ca_pem.present?
- variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true }
- variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables
+ .append(key: 'KUBE_URL', value: api_url)
+ .append(key: 'KUBE_TOKEN', value: token, public: false)
+ .append(key: 'KUBE_NAMESPACE', value: actual_namespace)
+ .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+
+ if ca_pem.present?
+ variables
+ .append(key: 'KUBE_CA_PEM', value: ca_pem)
+ .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true)
+ end
end
-
- variables
end
# Constructs a list of terminals from the reactive cache
diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb
index 67a988addbe..f05e606995d 100644
--- a/app/models/concerns/storage/legacy_namespace.rb
+++ b/app/models/concerns/storage/legacy_namespace.rb
@@ -7,29 +7,24 @@ module Storage
raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
end
- expires_full_path_cache
-
- # Move the namespace directory in all storage paths used by member projects
- repository_storage_paths.each do |repository_storage_path|
- # Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, full_path_was)
-
- # Ensure new directory exists before moving it (if there's a parent)
- gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent
+ parent_was = if parent_changed? && parent_id_was.present?
+ Namespace.find(parent_id_was) # raise NotFound early if needed
+ end
- unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
+ expires_full_path_cache
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
+ move_repositories
- # if we cannot move namespace directory we should rollback
- # db changes in order to prevent out of sync between db and fs
- raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
- end
+ if parent_changed?
+ former_parent_full_path = parent_was&.full_path
+ parent_full_path = parent&.full_path
+ Gitlab::UploadsTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
+ Gitlab::PagesTransfer.new.move_namespace(path, former_parent_full_path, parent_full_path)
+ else
+ Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path)
+ Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
end
- Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path)
- Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
-
remove_exports!
# If repositories moved successfully we need to
@@ -57,6 +52,26 @@ module Storage
private
+ def move_repositories
+ # Move the namespace directory in all storage paths used by member projects
+ repository_storage_paths.each do |repository_storage_path|
+ # Ensure old directory exists before moving it
+ gitlab_shell.add_namespace(repository_storage_path, full_path_was)
+
+ # Ensure new directory exists before moving it (if there's a parent)
+ gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent
+
+ unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
+
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
+
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
+ end
+ end
+ end
+
def old_repository_storage_paths
@old_repository_storage_paths ||= repository_storage_paths
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 2b0a88ac5b4..9517723d9d9 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -65,10 +65,9 @@ class Environment < ActiveRecord::Base
end
def predefined_variables
- [
- { key: 'CI_ENVIRONMENT_NAME', value: name, public: true },
- { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true }
- ]
+ Gitlab::Ci::Variables::Collection.new
+ .append(key: 'CI_ENVIRONMENT_NAME', value: name)
+ .append(key: 'CI_ENVIRONMENT_SLUG', value: slug)
end
def recently_updated_on_branch?(ref)
diff --git a/app/models/member.rb b/app/models/member.rb
index 36090676051..ec8156bbb01 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -55,7 +55,7 @@ class Member < ActiveRecord::Base
scope :active_without_invites, -> do
left_join_users
.where(users: { state: 'active' })
- .where(requested_at: nil)
+ .non_request
.reorder(nil)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index c2bae379a94..149ef7ec429 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -579,9 +579,10 @@ class MergeRequest < ActiveRecord::Base
return unless open?
old_diff_refs = self.diff_refs
+ new_diff = create_merge_request_diff
+
+ MergeRequests::MergeRequestDiffCacheService.new.execute(self, new_diff)
- create_merge_request_diff
- MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs
update_diff_discussion_positions(
diff --git a/app/models/project.rb b/app/models/project.rb
index 5f9d9785d64..5487194ed3e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1083,7 +1083,7 @@ class Project < ActiveRecord::Base
# Forked import is handled asynchronously
return if forked? && !force
- if gitlab_shell.add_repository(repository_storage, disk_path)
+ if gitlab_shell.create_repository(repository_storage, disk_path)
repository.after_create
true
else
@@ -1519,8 +1519,8 @@ class Project < ActiveRecord::Base
@errors = original_errors
end
- def add_export_job(current_user:)
- job_id = ProjectExportWorker.perform_async(current_user.id, self.id)
+ def add_export_job(current_user:, params: {})
+ job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
@@ -1572,29 +1572,30 @@ class Project < ActiveRecord::Base
end
def predefined_variables
- [
- { key: 'CI_PROJECT_ID', value: id.to_s, public: true },
- { key: 'CI_PROJECT_NAME', value: path, public: true },
- { key: 'CI_PROJECT_PATH', value: full_path, public: true },
- { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true },
- { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true },
- { key: 'CI_PROJECT_URL', value: web_url, public: true },
- { key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level), public: true }
- ]
+ visibility = Gitlab::VisibilityLevel.string_level(visibility_level)
+
+ Gitlab::Ci::Variables::Collection.new
+ .append(key: 'CI_PROJECT_ID', value: id.to_s)
+ .append(key: 'CI_PROJECT_NAME', value: path)
+ .append(key: 'CI_PROJECT_PATH', value: full_path)
+ .append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
+ .append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
+ .append(key: 'CI_PROJECT_URL', value: web_url)
+ .append(key: 'CI_PROJECT_VISIBILITY', value: visibility)
+ .concat(container_registry_variables)
+ .concat(auto_devops_variables)
end
def container_registry_variables
- return [] unless Gitlab.config.registry.enabled
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ return variables unless Gitlab.config.registry.enabled
- variables = [
- { key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port, public: true }
- ]
+ variables.append(key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port)
- if container_registry_enabled?
- variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_url, public: true }
+ if container_registry_enabled?
+ variables.append(key: 'CI_REGISTRY_IMAGE', value: container_registry_url)
+ end
end
-
- variables
end
def secret_variables_for(ref:, environment: nil)
@@ -1614,16 +1615,14 @@ class Project < ActiveRecord::Base
end
end
- def deployment_variables
- return [] unless deployment_platform
-
- deployment_platform.predefined_variables
+ def deployment_variables(environment: nil)
+ deployment_platform(environment: environment)&.predefined_variables || []
end
def auto_devops_variables
return [] unless auto_devops_enabled?
- (auto_devops || build_auto_devops)&.variables
+ (auto_devops || build_auto_devops)&.predefined_variables
end
def append_or_update_attribute(name, value)
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 112ed7ed434..ed6c1eddbc1 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -14,9 +14,12 @@ class ProjectAutoDevops < ActiveRecord::Base
domain.present? || instance_domain.present?
end
- def variables
- variables = []
- variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain.presence || instance_domain, public: true } if has_domain?
- variables
+ def predefined_variables
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ if has_domain?
+ variables.append(key: 'AUTO_DEVOPS_DOMAIN',
+ value: domain.presence || instance_domain)
+ end
+ end
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index e5035c81df0..601a6a077f5 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -161,11 +161,6 @@ class JiraService < IssueTrackerService
add_comment(data, jira_issue)
end
- # reason why service cannot be tested
- def disabled_title
- "Please fill in Password and Username."
- end
-
def test(_)
result = test_settings
success = result.present?
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index e412d15363d..20fed432e55 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -105,19 +105,19 @@ class KubernetesService < DeploymentService
def predefined_variables
config = YAML.dump(kubeconfig)
- variables = [
- { key: 'KUBE_URL', value: api_url, public: true },
- { key: 'KUBE_TOKEN', value: token, public: false },
- { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true },
- { key: 'KUBECONFIG', value: config, public: false, file: true }
- ]
-
- if ca_pem.present?
- variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true }
- variables << { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ Gitlab::Ci::Variables::Collection.new.tap do |variables|
+ variables
+ .append(key: 'KUBE_URL', value: api_url)
+ .append(key: 'KUBE_TOKEN', value: token, public: false)
+ .append(key: 'KUBE_NAMESPACE', value: actual_namespace)
+ .append(key: 'KUBECONFIG', value: config, public: false, file: true)
+
+ if ca_pem.present?
+ variables
+ .append(key: 'KUBE_CA_PEM', value: ca_pem)
+ .append(key: 'KUBE_CA_PEM_FILE', value: ca_pem, file: true)
+ end
end
-
- variables
end
# Constructs a list of terminals from the reactive cache
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 9c7b58dead5..4cf149ac044 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -39,10 +39,6 @@ class PipelinesEmailService < Service
project.pipelines.any?
end
- def disabled_title
- 'Please setup a pipeline on your repository.'
- end
-
def test_data(project, user)
data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
data[:user] = user.hook_attrs
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index f6041da986c..52e067cb44c 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -169,7 +169,7 @@ class ProjectWiki
private
def create_repo!(raw_repository)
- gitlab_shell.add_repository(project.repository_storage, disk_path)
+ gitlab_shell.create_repository(project.repository_storage, disk_path)
raise CouldNotCreateWikiError unless raw_repository.exists?
diff --git a/app/models/service.rb b/app/models/service.rb
index 99bf757ae44..2556db68146 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -162,11 +162,6 @@ class Service < ActiveRecord::Base
true
end
- # reason why service cannot be tested
- def disabled_title
- "Please setup a project repository."
- end
-
# Provide convenient accessor methods
# for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 46acdc5406c..a954564946b 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -1,11 +1,11 @@
module Files
class CreateService < Files::BaseService
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|
- create_transformed_commit(content_or_lfs_pointer)
- end
+ result = transformer.new_file(@file_path, @file_content)
+
+ create_transformed_commit(result.content)
end
private
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index a03c59f569d..13a1dee4173 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -3,11 +3,33 @@ module Files
UPDATE_FILE_ACTIONS = %w(update move delete).freeze
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(
current_user,
message: @commit_message,
branch_name: @branch_name,
- actions: params[:actions],
+ actions: actions,
author_email: @author_email,
author_name: @author_name,
start_project: @start_project,
@@ -17,8 +39,6 @@ module Files
raise_error(e)
end
- private
-
def validate!
super
diff --git a/app/services/lfs/file_modification_handler.rb b/app/services/lfs/file_modification_handler.rb
deleted file mode 100644
index fe9091a6e5d..00000000000
--- a/app/services/lfs/file_modification_handler.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module Lfs
- class FileModificationHandler
- attr_reader :project, :branch_name
-
- delegate :repository, to: :project
-
- def initialize(project, branch_name)
- @project = project
- @branch_name = branch_name
- end
-
- def new_file(file_path, file_content)
- if project.lfs_enabled? && lfs_file?(file_path)
- lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(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) if success
- else
- yield(file_content)
- end
- end
-
- private
-
- def lfs_file?(file_path)
- repository.attributes_at(branch_name, file_path)['filter'] == 'lfs'
- end
-
- def create_lfs_object!(lfs_pointer_file, file_content)
- LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object|
- lfs_object.file = CarrierWaveStringFile.new(file_content)
- end
- end
-
- def link_lfs_object!(lfs_object)
- project.lfs_objects << lfs_object
- end
- end
-end
diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb
new file mode 100644
index 00000000000..69281ee3137
--- /dev/null
+++ b/app/services/lfs/file_transformer.rb
@@ -0,0 +1,66 @@
+module Lfs
+ # 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
+
+ delegate :repository, to: :project
+
+ def initialize(project, branch_name)
+ @project = project
+ @branch_name = branch_name
+ end
+
+ def new_file(file_path, file_content, encoding: nil)
+ 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_object = create_lfs_object!(lfs_pointer_file, file_content)
+
+ link_lfs_object!(lfs_object)
+
+ Result.new(content: lfs_pointer_file.pointer, encoding: 'text')
+ else
+ 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
+
+ private
+
+ def lfs_file?(file_path)
+ cached_attributes.attributes(file_path)['filter'] == 'lfs'
+ end
+
+ def cached_attributes
+ @cached_attributes ||= Gitlab::Git::AttributesAtRefParser.new(repository, branch_name)
+ end
+
+ def create_lfs_object!(lfs_pointer_file, file_content)
+ LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object|
+ lfs_object.file = CarrierWaveStringFile.new(file_content)
+ end
+ end
+
+ def link_lfs_object!(lfs_object)
+ project.lfs_objects << lfs_object
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb
index 2945a7fd4e4..10aa9ae609c 100644
--- a/app/services/merge_requests/merge_request_diff_cache_service.rb
+++ b/app/services/merge_requests/merge_request_diff_cache_service.rb
@@ -1,8 +1,17 @@
module MergeRequests
class MergeRequestDiffCacheService
- def execute(merge_request)
+ def execute(merge_request, new_diff)
# Executing the iteration we cache all the highlighted diff information
merge_request.diffs.diff_files.to_a
+
+ # Remove cache for all diffs on this MR. Do not use the association on the
+ # model, as that will interfere with other actions happening when
+ # reloading the diff.
+ MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
+ next if merge_request_diff == new_diff
+
+ merge_request_diff.diffs.clear_cache!
+ end
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e07ecda27b5..ab94db2c1e5 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -208,9 +208,9 @@ class NotificationService
def new_access_request(member)
return true unless member.notifiable?(:subscription)
- recipients = member.source.members.owners_and_masters
+ recipients = member.source.members.active_without_invites.owners_and_masters
if fallback_to_group_owners_masters?(recipients, member)
- recipients = member.source.group.members.owners_and_masters
+ recipients = member.source.group.members.active_without_invites.owners_and_masters
end
recipients.each { |recipient| deliver_access_request_email(recipient, member) }
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index af41ce82f65..d16aa3de639 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -26,7 +26,7 @@ module Projects
end
def project_tree_saver
- Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared)
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project, current_user: @current_user, shared: @shared, params: @params)
end
def uploads_saver
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 185e9d7b35d..37269862de6 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -9,6 +9,10 @@
%span.runner-state.runner-state-specific
Specific
+- add_to_breadcrumbs _("Runners"), admin_runners_path
+- breadcrumb_title "##{@runner.id}"
+- @no_container = true
+
- if @runner.shared?
.bs-callout.bs-callout-success
%h4 This Runner will process jobs from ALL UNASSIGNED projects
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index f9bfc01f213..8680ec2e298 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -2,8 +2,12 @@
- blob = discussion.blob
- discussions = { discussion.original_line_code => [discussion] }
- diff_file_class = diff_file.text? ? 'text-file' : 'js-image-file'
+- diff_data = {}
+- expanded = discussion.expanded? || local_assigns.fetch(:expanded, nil)
+- unless expanded
+ - diff_data = { lines_path: project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) }
-.diff-file.file-holder{ class: diff_file_class }
+.diff-file.file-holder{ class: diff_file_class, data: diff_data }
.js-file-title.file-title.file-title-flex-parent
.file-header-content
= render "projects/diffs/file_header", diff_file: diff_file, url: discussion_path(discussion), show_toggle: false
@@ -11,17 +15,24 @@
- if diff_file.text?
.diff-content.code.js-syntax-highlight
%table
- = render partial: "projects/diffs/line",
- collection: discussion.truncated_diff_lines,
- as: :line,
- locals: { diff_file: diff_file,
- discussions: discussions,
- discussion_expanded: true,
- plain: true }
+ - if expanded
+ - discussions = { discussion.original_line_code => [discussion] }
+ = render partial: "projects/diffs/line",
+ collection: discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: diff_file,
+ discussions: discussions,
+ discussion_expanded: true,
+ plain: true }
+ - else
+ %tr.line_holder.line-holder-placeholder
+ %td.old_line.diff-line-num
+ %td.new_line.diff-line-num
+ %td.line_content
+ .js-code-placeholder
+ = render "discussions/diff_discussion", discussions: [discussion], expanded: true
- else
- partial = (diff_file.new_file? || diff_file.deleted_file?) ? 'single_image_diff' : 'replaced_image_diff'
-
= render partial: "projects/diffs/#{partial}", locals: { diff_file: diff_file, position: discussion.position.to_json, click_to_comment: false }
-
.note-container
= render partial: "discussions/notes", locals: { discussion: discussion, show_toggle: false, show_image_comment_badge: true, disable_collapse_class: true }
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 8b9fa3d6b05..e9589213f80 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -8,7 +8,7 @@
.discussion.js-toggle-container{ data: { discussion_id: discussion.id } }
.discussion-header
.discussion-actions
- %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button" }
+ %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button", class: ("js-toggle-lazy-diff" unless expanded) }
- if expanded
= icon("chevron-up")
- else
diff --git a/app/views/peek/views/_gc.html.haml b/app/views/peek/views/_gc.html.haml
new file mode 100644
index 00000000000..9fc83e56ee7
--- /dev/null
+++ b/app/views/peek/views/_gc.html.haml
@@ -0,0 +1,7 @@
+- local_assigns.fetch(:view)
+
+%span.bold
+ %span{ title: 'Invoke Time', data: { defer_to: "#{view.defer_key}-gc_time" } }...
+ \/
+ %span{ title: 'Invoke Count', data: { defer_to: "#{view.defer_key}-invokes" } }...
+gc
diff --git a/app/views/peek/views/_gitaly.html.haml b/app/views/peek/views/_gitaly.html.haml
index a7d040d6821..945bb287429 100644
--- a/app/views/peek/views/_gitaly.html.haml
+++ b/app/views/peek/views/_gitaly.html.haml
@@ -1,7 +1,17 @@
- local_assigns.fetch(:view)
-%strong
- %span{ data: { defer_to: "#{view.defer_key}-duration" } } ...
+%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-gitaly-details' } }
+ %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
\/
- %span{ data: { defer_to: "#{view.defer_key}-calls" } } ...
- Gitaly
+ %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
+#modal-peek-gitaly-details.modal{ tabindex: -1, role: 'dialog' }
+ .modal-dialog.modal-full
+ .modal-content
+ .modal-header
+ %button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' }
+ %span{ 'aria-hidden' => 'true' }
+ &times;
+ %h4
+ Gitaly requests
+ .modal-body{ data: { defer_to: "#{view.defer_key}-details" } }...
+gitaly
diff --git a/app/views/peek/views/_redis.html.haml b/app/views/peek/views/_redis.html.haml
new file mode 100644
index 00000000000..f7fba6c95fc
--- /dev/null
+++ b/app/views/peek/views/_redis.html.haml
@@ -0,0 +1,7 @@
+- local_assigns.fetch(:view)
+
+%span.bold
+ %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
+ \/
+ %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
+redis
diff --git a/app/views/peek/views/_sidekiq.html.haml b/app/views/peek/views/_sidekiq.html.haml
new file mode 100644
index 00000000000..7efbc05890d
--- /dev/null
+++ b/app/views/peek/views/_sidekiq.html.haml
@@ -0,0 +1,7 @@
+- local_assigns.fetch(:view)
+
+%span.bold
+ %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
+ \/
+ %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
+sidekiq
diff --git a/app/views/peek/views/_sql.html.haml b/app/views/peek/views/_sql.html.haml
index dd8b524064f..36583df898a 100644
--- a/app/views/peek/views/_sql.html.haml
+++ b/app/views/peek/views/_sql.html.haml
@@ -1,13 +1,14 @@
-%strong
- %a.js-toggle-modal-peek-sql
- %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
- \/
- %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
+%button.btn-blank.btn-link.bold{ type: 'button', data: { toggle: 'modal', target: '#modal-peek-pg-queries' } }
+ %span{ data: { defer_to: "#{view.defer_key}-duration" } }...
+ \/
+ %span{ data: { defer_to: "#{view.defer_key}-calls" } }...
#modal-peek-pg-queries.modal{ tabindex: -1 }
.modal-dialog.modal-full
.modal-content
.modal-header
- %button.close.btn.btn-link.btn-sm{ type: 'button', data: { dismiss: 'modal' } } X
+ %button.close{ type: 'button', data: { dismiss: 'modal' }, 'aria-label' => 'Close' }
+ %span{ 'aria-hidden' => 'true' }
+ &times;
%h4
SQL queries
.modal-body{ data: { defer_to: "#{view.defer_key}-queries" } }...
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 053ea24b848..684b082efbb 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -15,11 +15,6 @@
.footer-block.row-content-block
= service_save_button(@service)
&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'
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index c100852374a..0b502143e5d 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -4,10 +4,11 @@ class ProjectExportWorker
sidekiq_options retry: 3
- def perform(current_user_id, project_id)
+ def perform(current_user_id, project_id, params = {})
+ params = params.with_indifferent_access
current_user = User.find(current_user_id)
project = Project.find(project_id)
- ::Projects::ImportExport::ExportService.new(project, current_user).execute
+ ::Projects::ImportExport::ExportService.new(project, current_user, params).execute
end
end