diff options
Diffstat (limited to 'app')
46 files changed, 191 insertions, 1246 deletions
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js deleted file mode 100644 index dd60e2c7684..00000000000 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ /dev/null @@ -1,65 +0,0 @@ -/* global CommentsStore */ - -import $ from 'jquery'; -import Vue from 'vue'; -import { __ } from '~/locale'; - -const CommentAndResolveBtn = Vue.extend({ - props: { - discussionId: { - type: String, - required: true, - }, - }, - data() { - return { - textareaIsEmpty: true, - discussion: {}, - }; - }, - computed: { - showButton() { - if (this.discussion) { - return this.discussion.isResolvable(); - } - return false; - }, - isDiscussionResolved() { - return this.discussion.isResolved(); - }, - buttonText() { - if (this.textareaIsEmpty) { - return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread'); - } - return this.isDiscussionResolved - ? __('Comment & unresolve thread') - : __('Comment & resolve thread'); - }, - }, - created() { - if (this.discussionId) { - this.discussion = CommentsStore.state[this.discussionId]; - } - }, - mounted() { - if (!this.discussionId) return; - - const $textarea = $( - `.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`, - ); - this.textareaIsEmpty = $textarea.val() === ''; - - $textarea.on('input.comment-and-resolve-btn', () => { - this.textareaIsEmpty = $textarea.val() === ''; - }); - }, - destroyed() { - if (!this.discussionId) return; - - $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off( - 'input.comment-and-resolve-btn', - ); - }, -}); - -Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js deleted file mode 100644 index b5a781cbc92..00000000000 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ /dev/null @@ -1,189 +0,0 @@ -/* global CommentsStore */ - -import $ from 'jquery'; -import Vue from 'vue'; -import collapseIcon from '../icons/collapse_icon.svg'; -import Notes from '../../notes'; -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; -import { n__ } from '~/locale'; - -const DiffNoteAvatars = Vue.extend({ - components: { - userAvatarImage, - }, - props: { - discussionId: { - type: String, - required: true, - }, - }, - data() { - return { - isVisible: false, - lineType: '', - storeState: CommentsStore.state, - shownAvatars: 3, - collapseIcon, - }; - }, - computed: { - discussionClassName() { - return `js-diff-avatars-${this.discussionId}`; - }, - notesSubset() { - let notes = []; - - if (this.discussion) { - notes = Object.keys(this.discussion.notes) - .slice(0, this.shownAvatars) - .map(noteId => this.discussion.notes[noteId]); - } - - return notes; - }, - extraNotesTitle() { - if (this.discussion) { - const extra = this.discussion.notesCount() - this.shownAvatars; - - return n__('%d more comment', '%d more comments', extra); - } - - return ''; - }, - discussion() { - return this.storeState[this.discussionId]; - }, - notesCount() { - if (this.discussion) { - return this.discussion.notesCount(); - } - - return 0; - }, - moreText() { - const plusSign = this.notesCount < 100 ? '+' : ''; - - return `${plusSign}${this.notesCount - this.shownAvatars}`; - }, - }, - watch: { - storeState: { - handler() { - this.$nextTick(() => { - $('.has-tooltip', this.$el).tooltip('_fixTitle'); - - // We need to add/remove a class to an element that is outside the Vue instance - this.addNoCommentClass(); - }); - }, - deep: true, - }, - }, - mounted() { - this.$nextTick(() => { - this.addNoCommentClass(); - this.setDiscussionVisible(); - - this.lineType = $(this.$el) - .closest('.diff-line-num') - .hasClass('old_line') - ? 'old' - : 'new'; - }); - - $(document).on('toggle.comments', () => { - this.$nextTick(() => { - this.setDiscussionVisible(); - }); - }); - }, - beforeDestroy() { - this.addNoCommentClass(); - $(document).off('toggle.comments'); - }, - methods: { - clickedAvatar(e) { - Notes.instance.onAddDiffNote(e); - - // Toggle the active state of the toggle all button - this.toggleDiscussionsToggleState(); - - this.$nextTick(() => { - this.setDiscussionVisible(); - - $('.has-tooltip', this.$el).tooltip('_fixTitle'); - $('.has-tooltip', this.$el).tooltip('hide'); - }); - }, - addNoCommentClass() { - const { notesCount } = this; - - $(this.$el) - .closest('.js-avatar-container') - .toggleClass('no-comment-btn', notesCount > 0) - .nextUntil('.js-avatar-container') - .toggleClass('no-comment-btn', notesCount > 0); - }, - toggleDiscussionsToggleState() { - const $notesHolders = $(this.$el) - .closest('.code') - .find('.notes_holder'); - const $visibleNotesHolders = $notesHolders.filter(':visible'); - const $toggleDiffCommentsBtn = $(this.$el) - .closest('.diff-file') - .find('.js-toggle-diff-comments'); - - $toggleDiffCommentsBtn.toggleClass( - 'active', - $notesHolders.length === $visibleNotesHolders.length, - ); - }, - setDiscussionVisible() { - this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is( - ':visible', - ); - }, - getTooltipText(note) { - return `${note.authorName}: ${note.noteTruncated}`; - }, - }, - template: ` - <div class="diff-comment-avatar-holders" - :class="discussionClassName" - v-show="notesCount !== 0"> - <div v-if="!isVisible"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image - v-for="note in notesSubset" - :key="note.id" - class="diff-comment-avatar js-diff-comment-avatar" - @click.native="clickedAvatar($event)" - :img-src="note.authorAvatar" - :tooltip-text="getTooltipText(note)" - :data-line-type="lineType" - :size="19" - data-html="true" - /> - <span v-if="notesCount > shownAvatars" - class="diff-comments-more-count has-tooltip js-diff-comment-avatar" - data-container="body" - data-placement="top" - ref="extraComments" - role="button" - :data-line-type="lineType" - :title="extraNotesTitle" - @click="clickedAvatar($event)">{{ moreText }}</span> - </div> - <button class="diff-notes-collapse js-diff-comment-avatar" - type="button" - aria-label="Show comments" - :data-line-type="lineType" - @click="clickedAvatar($event)" - v-if="isVisible" - v-html="collapseIcon"> - </button> - </div> - `, -}); - -Vue.component('diff-note-avatars', DiffNoteAvatars); diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js deleted file mode 100644 index 1de00c9f08b..00000000000 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable func-names, no-continue */ -/* global CommentsStore */ - -import $ from 'jquery'; -import 'vendor/jquery.scrollTo'; -import Vue from 'vue'; -import { __ } from '~/locale'; - -import DiscussionMixins from '../mixins/discussion'; - -const JumpToDiscussion = Vue.extend({ - mixins: [DiscussionMixins], - props: { - discussionId: { - type: String, - required: true, - }, - }, - data() { - return { - discussions: CommentsStore.state, - discussion: {}, - }; - }, - computed: { - buttonText() { - if (this.discussionId) { - return __('Jump to next unresolved thread'); - } - return __('Jump to first unresolved thread'); - }, - allResolved() { - return this.unresolvedDiscussionCount === 0; - }, - showButton() { - if (this.discussionId) { - if (this.unresolvedDiscussionCount > 1) { - return true; - } - return this.discussionId !== this.lastResolvedId; - } - return this.unresolvedDiscussionCount >= 1; - }, - lastResolvedId() { - let lastId; - Object.keys(this.discussions).forEach(discussionId => { - const discussion = this.discussions[discussionId]; - - if (!discussion.isResolved()) { - lastId = discussion.id; - } - }); - return lastId; - }, - }, - created() { - this.discussion = this.discussions[this.discussionId]; - }, - methods: { - jumpToNextUnresolvedDiscussion() { - let discussionsSelector; - let discussionIdsInScope; - let firstUnresolvedDiscussionId; - let nextUnresolvedDiscussionId; - let activeTab = window.mrTabs.currentAction; - let hasDiscussionsToJumpTo = true; - let jumpToFirstDiscussion = !this.discussionId; - - const discussionIdsForElements = function(elements) { - return elements - .map(function() { - return $(this).attr('data-discussion-id'); - }) - .toArray(); - }; - - const { discussions } = this; - - if (activeTab === 'diffs') { - discussionsSelector = '.diffs .notes[data-discussion-id]'; - discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); - - let unresolvedDiscussionCount = 0; - - for (let i = 0; i < discussionIdsInScope.length; i += 1) { - const discussionId = discussionIdsInScope[i]; - const discussion = discussions[discussionId]; - if (discussion && !discussion.isResolved()) { - unresolvedDiscussionCount += 1; - } - } - - if (this.discussionId && !this.discussion.isResolved()) { - // If this is the last unresolved discussion on the diffs tab, - // there are no discussions to jump to. - if (unresolvedDiscussionCount === 1) { - hasDiscussionsToJumpTo = false; - } - } else if (unresolvedDiscussionCount === 0) { - // If there are no unresolved discussions on the diffs tab at all, - // there are no discussions to jump to. - hasDiscussionsToJumpTo = false; - } - } else if (activeTab !== 'show') { - // If we are on the commits or builds tabs, - // there are no discussions to jump to. - hasDiscussionsToJumpTo = false; - } - - if (!hasDiscussionsToJumpTo) { - // If there are no discussions to jump to on the current page, - // switch to the notes tab and jump to the first discussion there. - window.mrTabs.activateTab('show'); - activeTab = 'show'; - jumpToFirstDiscussion = true; - } - - if (activeTab === 'show') { - discussionsSelector = '.discussion[data-discussion-id]'; - discussionIdsInScope = discussionIdsForElements($(discussionsSelector)); - } - - let currentDiscussionFound = false; - for (let i = 0; i < discussionIdsInScope.length; i += 1) { - const discussionId = discussionIdsInScope[i]; - const discussion = discussions[discussionId]; - - if (!discussion) { - // Discussions for comments on commits in this MR don't have a resolved status. - continue; - } - - if (!firstUnresolvedDiscussionId && !discussion.isResolved()) { - firstUnresolvedDiscussionId = discussionId; - - if (jumpToFirstDiscussion) { - break; - } - } - - if (!jumpToFirstDiscussion) { - if (currentDiscussionFound) { - if (!discussion.isResolved()) { - nextUnresolvedDiscussionId = discussionId; - break; - } else { - continue; - } - } - - if (discussionId === this.discussionId) { - currentDiscussionFound = true; - } - } - } - - nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId; - - if (!nextUnresolvedDiscussionId) { - return; - } - - let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`); - - if (activeTab === 'show') { - $target = $target.closest('.note-discussion'); - - // If the next discussion is closed, toggle it open. - if ($target.find('.js-toggle-content').is(':hidden')) { - $target.find('.js-toggle-button i').trigger('click'); - } - } else if (activeTab === 'diffs') { - // Resolved discussions are hidden in the diffs tab by default. - // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab. - // When jumping between unresolved discussions on the diffs tab, we show them. - $target.closest('.content').show(); - - const $notesHolder = $target.closest('tr.notes_holder'); - - // Image diff discussions does not use notes_holder - // so we should keep original $target value in those cases - if ($notesHolder.length > 0) { - $target = $notesHolder; - } - - $target.show(); - - // If we are on the diffs tab, we don't scroll to the discussion itself, but to - // 4 diff lines above it: the line the discussion was in response to + 3 context - let prevEl; - for (let i = 0; i < 4; i += 1) { - prevEl = $target.prev(); - - // If the discussion doesn't have 4 lines above it, we'll have to do with fewer. - if (!prevEl.hasClass('line_holder')) { - break; - } - - $target = prevEl; - } - } - - $.scrollTo($target, { - offset: -150, - }); - }, - }, -}); - -Vue.component('jump-to-discussion', JumpToDiscussion); diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js deleted file mode 100644 index e0c09aa0eee..00000000000 --- a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js +++ /dev/null @@ -1,28 +0,0 @@ -/* global CommentsStore */ - -import Vue from 'vue'; - -const NewIssueForDiscussion = Vue.extend({ - props: { - discussionId: { - type: String, - required: true, - }, - }, - data() { - return { - discussions: CommentsStore.state, - }; - }, - computed: { - discussion() { - return this.discussions[this.discussionId]; - }, - showButton() { - if (this.discussion) return !this.discussion.isResolved(); - return false; - }, - }, -}); - -Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js deleted file mode 100644 index 0943712d0c5..00000000000 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ /dev/null @@ -1,145 +0,0 @@ -/* global CommentsStore */ -/* global ResolveService */ - -import $ from 'jquery'; -import Vue from 'vue'; -import { deprecatedCreateFlash as Flash } from '../../flash'; -import { sprintf, __ } from '~/locale'; - -const ResolveBtn = Vue.extend({ - props: { - noteId: { - type: Number, - required: true, - }, - discussionId: { - type: String, - required: true, - }, - resolved: { - type: Boolean, - required: true, - }, - canResolve: { - type: Boolean, - required: true, - }, - resolvedBy: { - type: String, - required: true, - }, - authorName: { - type: String, - required: true, - }, - authorAvatar: { - type: String, - required: true, - }, - noteTruncated: { - type: String, - required: true, - }, - }, - data() { - return { - discussions: CommentsStore.state, - loading: false, - }; - }, - computed: { - discussion() { - return this.discussions[this.discussionId]; - }, - note() { - return this.discussion ? this.discussion.getNote(this.noteId) : {}; - }, - buttonText() { - if (this.isResolved) { - return sprintf(__('Resolved by %{resolvedByName}'), { - resolvedByName: this.resolvedByName, - }); - } else if (this.canResolve) { - return __('Mark as resolved'); - } - - return __('Unable to resolve'); - }, - isResolved() { - if (this.note) { - return this.note.resolved; - } - - return false; - }, - resolvedByName() { - return this.note.resolved_by; - }, - }, - watch: { - discussions: { - handler: 'updateTooltip', - deep: true, - }, - }, - mounted() { - $(this.$refs.button).tooltip({ - container: 'body', - }); - }, - beforeDestroy() { - CommentsStore.delete(this.discussionId, this.noteId); - }, - created() { - CommentsStore.create({ - discussionId: this.discussionId, - noteId: this.noteId, - canResolve: this.canResolve, - resolved: this.resolved, - resolvedBy: this.resolvedBy, - authorName: this.authorName, - authorAvatar: this.authorAvatar, - noteTruncated: this.noteTruncated, - }); - }, - methods: { - updateTooltip() { - this.$nextTick(() => { - $(this.$refs.button) - .tooltip('hide') - .tooltip('_fixTitle'); - }); - }, - resolve() { - if (!this.canResolve) return; - - let promise; - this.loading = true; - - if (this.isResolved) { - promise = ResolveService.unresolve(this.noteId); - } else { - promise = ResolveService.resolve(this.noteId); - } - - promise - .then(resp => resp.json()) - .then(data => { - this.loading = false; - - const resolvedBy = data ? data.resolved_by : null; - - CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolvedBy); - this.discussion.updateHeadline(data); - gl.mrWidget.checkStatus(); - this.updateTooltip(); - }) - .catch( - () => - new Flash(__('An error occurred when trying to resolve a comment. Please try again.')), - ); - }, - }, -}); - -Vue.component('resolve-btn', ResolveBtn); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js deleted file mode 100644 index f960853b25b..00000000000 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js +++ /dev/null @@ -1,28 +0,0 @@ -/* global CommentsStore */ - -import Vue from 'vue'; - -import DiscussionMixins from '../mixins/discussion'; - -window.ResolveCount = Vue.extend({ - mixins: [DiscussionMixins], - props: { - loggedOut: { - type: Boolean, - required: true, - }, - }, - data() { - return { - discussions: CommentsStore.state, - }; - }, - computed: { - allResolved() { - return this.resolvedDiscussionCount === this.discussionCount; - }, - resolvedCountText() { - return this.discussionCount === 1 ? 'discussion' : 'discussions'; - }, - }, -}); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js deleted file mode 100644 index 92862d4c933..00000000000 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ /dev/null @@ -1,72 +0,0 @@ -/* eslint-disable func-names, new-cap */ - -import $ from 'jquery'; -import Vue from 'vue'; -import './models/discussion'; -import './models/note'; -import './stores/comments'; -import './services/resolve'; -import './mixins/discussion'; -import './components/comment_resolve_btn'; -import './components/jump_to_discussion'; -import './components/resolve_btn'; -import './components/resolve_count'; -import './components/diff_note_avatars'; -import './components/new_issue_for_discussion'; - -export default () => { - const projectPathHolder = - document.querySelector('.merge-request') || document.querySelector('.commit-box'); - const { projectPath } = projectPathHolder.dataset; - const COMPONENT_SELECTOR = - 'resolve-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn'; - - window.gl = window.gl || {}; - window.gl.diffNoteApps = {}; - - window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath); - - gl.diffNotesCompileComponents = () => { - $('diff-note-avatars').each(function() { - const tmp = Vue.extend({ - template: $(this).get(0).outerHTML, - }); - const tmpApp = new tmp().$mount(); - - $(this).replaceWith(tmpApp.$el); - $(tmpApp.$el).one('remove.vue', () => { - tmpApp.$destroy(); - tmpApp.$el.remove(); - }); - }); - - const $components = $(COMPONENT_SELECTOR).filter(function() { - return $(this).closest('resolve-count').length !== 1; - }); - - if ($components) { - $components.each(function() { - const $this = $(this); - const noteId = $this.attr(':note-id'); - const discussionId = $this.attr(':discussion-id'); - - if ($this.is('comment-and-resolve-btn') && !discussionId) return; - - const tmp = Vue.extend({ - template: $this.get(0).outerHTML, - }); - const tmpApp = new tmp().$mount(); - - if (noteId) { - gl.diffNoteApps[`note_${noteId}`] = tmpApp; - } - - $this.replaceWith(tmpApp.$el); - }); - } - }; - - gl.diffNotesCompileComponents(); - - $(window).trigger('resize.nav'); -}; diff --git a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg deleted file mode 100644 index bd4b393cfaa..00000000000 --- a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg> diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js deleted file mode 100644 index ef3001393cf..00000000000 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js +++ /dev/null @@ -1,37 +0,0 @@ -/* eslint-disable guard-for-in, no-restricted-syntax, */ - -const DiscussionMixins = { - computed: { - discussionCount() { - return Object.keys(this.discussions).length; - }, - resolvedDiscussionCount() { - let resolvedCount = 0; - - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; - - if (discussion.isResolved()) { - resolvedCount += 1; - } - } - - return resolvedCount; - }, - unresolvedDiscussionCount() { - let unresolvedCount = 0; - - for (const discussionId in this.discussions) { - const discussion = this.discussions[discussionId]; - - if (!discussion.isResolved()) { - unresolvedCount += 1; - } - } - - return unresolvedCount; - }, - }, -}; - -export default DiscussionMixins; diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js deleted file mode 100644 index 97296a40d6e..00000000000 --- a/app/assets/javascripts/diff_notes/models/discussion.js +++ /dev/null @@ -1,99 +0,0 @@ -/* eslint-disable guard-for-in, no-restricted-syntax */ -/* global NoteModel */ - -import $ from 'jquery'; -import Vue from 'vue'; -import { localTimeAgo } from '../../lib/utils/datetime_utility'; - -class DiscussionModel { - constructor(discussionId) { - this.id = discussionId; - this.notes = {}; - this.loading = false; - this.canResolve = false; - } - - createNote(noteObj) { - Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj)); - } - - deleteNote(noteId) { - Vue.delete(this.notes, noteId); - } - - getNote(noteId) { - return this.notes[noteId]; - } - - notesCount() { - return Object.keys(this.notes).length; - } - - isResolved() { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (!note.resolved) { - return false; - } - } - return true; - } - - resolveAllNotes(resolvedBy) { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (!note.resolved) { - note.resolved = true; - note.resolved_by = resolvedBy; - } - } - } - - unResolveAllNotes() { - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (note.resolved) { - note.resolved = false; - note.resolved_by = null; - } - } - } - - updateHeadline(data) { - const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`; - const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`); - - if (data.discussion_headline_html) { - if ($discussionHeadline.length) { - $discussionHeadline.replaceWith(data.discussion_headline_html); - } else { - $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); - } - - localTimeAgo($('.js-timeago', `${discussionSelector}`)); - } else { - $discussionHeadline.remove(); - } - } - - isResolvable() { - if (!this.canResolve) { - return false; - } - - for (const noteId in this.notes) { - const note = this.notes[noteId]; - - if (note.canResolve) { - return true; - } - } - - return false; - } -} - -window.DiscussionModel = DiscussionModel; diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js deleted file mode 100644 index 825a69deeec..00000000000 --- a/app/assets/javascripts/diff_notes/models/note.js +++ /dev/null @@ -1,14 +0,0 @@ -class NoteModel { - constructor(discussionId, noteObj) { - this.discussionId = discussionId; - this.id = noteObj.noteId; - this.canResolve = noteObj.canResolve; - this.resolved = noteObj.resolved; - this.resolved_by = noteObj.resolvedBy; - this.authorName = noteObj.authorName; - this.authorAvatar = noteObj.authorAvatar; - this.noteTruncated = noteObj.noteTruncated; - } -} - -window.NoteModel = NoteModel; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js deleted file mode 100644 index d6975963977..00000000000 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ /dev/null @@ -1,86 +0,0 @@ -/* global CommentsStore */ - -import Vue from 'vue'; -import { deprecatedCreateFlash as Flash } from '../../flash'; -import { __ } from '~/locale'; - -window.gl = window.gl || {}; - -class ResolveServiceClass { - constructor(root) { - this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`); - this.discussionResource = Vue.resource( - `${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`, - ); - } - - resolve(noteId) { - return this.noteResource.save({ noteId }, {}); - } - - unresolve(noteId) { - return this.noteResource.delete({ noteId }, {}); - } - - toggleResolveForDiscussion(mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; - const isResolved = discussion.isResolved(); - let promise; - - if (isResolved) { - promise = this.unResolveAll(mergeRequestId, discussionId); - } else { - promise = this.resolveAll(mergeRequestId, discussionId); - } - - promise - .then(resp => resp.json()) - .then(data => { - discussion.loading = false; - const resolvedBy = data ? data.resolved_by : null; - - if (isResolved) { - discussion.unResolveAllNotes(); - } else { - discussion.resolveAllNotes(resolvedBy); - } - - if (gl.mrWidget) gl.mrWidget.checkStatus(); - discussion.updateHeadline(data); - }) - .catch( - () => - new Flash(__('An error occurred when trying to resolve a discussion. Please try again.')), - ); - } - - resolveAll(mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; - - discussion.loading = true; - - return this.discussionResource.save( - { - mergeRequestId, - discussionId, - }, - {}, - ); - } - - unResolveAll(mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId]; - - discussion.loading = true; - - return this.discussionResource.delete( - { - mergeRequestId, - discussionId, - }, - {}, - ); - } -} - -gl.DiffNotesResolveServiceClass = ResolveServiceClass; diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js deleted file mode 100644 index 9bde18c4edf..00000000000 --- a/app/assets/javascripts/diff_notes/stores/comments.js +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable no-restricted-syntax, guard-for-in */ -/* global DiscussionModel */ - -import Vue from 'vue'; - -window.CommentsStore = { - state: {}, - get(discussionId, noteId) { - return this.state[discussionId].getNote(noteId); - }, - createDiscussion(discussionId, canResolve) { - let discussion = this.state[discussionId]; - if (!this.state[discussionId]) { - discussion = new DiscussionModel(discussionId); - Vue.set(this.state, discussionId, discussion); - } - - if (canResolve !== undefined) { - discussion.canResolve = canResolve; - } - - return discussion; - }, - create(noteObj) { - const discussion = this.createDiscussion(noteObj.discussionId); - - discussion.createNote(noteObj); - }, - update(discussionId, noteId, resolved, resolvedBy) { - const discussion = this.state[discussionId]; - const note = discussion.getNote(noteId); - note.resolved = resolved; - note.resolved_by = resolvedBy; - }, - delete(discussionId, noteId) { - const discussion = this.state[discussionId]; - discussion.deleteNote(noteId); - - if (discussion.notesCount() === 0) { - Vue.delete(this.state, discussionId); - } - }, - unresolvedDiscussionIds() { - const ids = []; - - for (const discussionId in this.state) { - const discussion = this.state[discussionId]; - - if (!discussion.isResolved()) { - ids.push(discussion.id); - } - } - - return ids; - }, -}; diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 091704c49f7..02396a4ba1b 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -1,32 +1,26 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { escape } from 'lodash'; -import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; +import { GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import { deprecatedCreateFlash as createFlash } from '~/flash'; import { hasDiff } from '~/helpers/diffs_helper'; import eventHub from '../../notes/event_hub'; import DiffFileHeader from './diff_file_header.vue'; import DiffContent from './diff_content.vue'; import { diffViewerErrors } from '~/ide/constants'; -import { GENERIC_ERROR, DIFF_FILE } from '../i18n'; export default { components: { DiffFileHeader, DiffContent, - GlButton, GlLoadingIcon, }, directives: { SafeHtml, }, mixins: [glFeatureFlagsMixin()], - i18n: { - genericError: GENERIC_ERROR, - ...DIFF_FILE, - }, props: { file: { type: Object, @@ -59,7 +53,7 @@ export default { ...mapGetters('diffs', ['getDiffFileDiscussions']), viewBlobLink() { return sprintf( - this.$options.i18n.blobView, + __('You can %{linkStart}view the blob%{linkEnd} instead.'), { linkStart: `<a href="${escape(this.file.view_path)}">`, linkEnd: '</a>', @@ -81,7 +75,9 @@ export default { }, forkMessage() { return sprintf( - this.$options.i18n.editInFork, + __( + "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.", + ), { tag_start: '<span class="js-file-fork-suggestion-section-action">', tag_end: '</span>', @@ -152,7 +148,7 @@ export default { }) .catch(() => { this.isLoadingCollapsedDiff = false; - createFlash(this.$options.i18n.genericError); + createFlash(__('Something went wrong on our end. Please try again!')); }); }, showForkMessage() { @@ -192,14 +188,14 @@ export default { <a :href="file.fork_path" class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success" - >{{ $options.i18n.fork }}</a + >{{ __('Fork') }}</a > <button class="js-cancel-fork-suggestion-button btn btn-grouped" type="button" @click="hideForkMessage" > - {{ $options.i18n.cancel }} + {{ __('Cancel') }} </button> </div> <gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" /> @@ -209,16 +205,11 @@ export default { <div v-safe-html="errorMessage" class="nothing-here-block"></div> </div> <template v-else> - <div - v-show="isCollapsed" - class="gl-p-7 gl-bg-gray-10 gl-text-center collapsed-file-warning" - > - <p class="gl-mb-8 gl-mt-5"> - {{ $options.i18n.collapsed }} - </p> - <gl-button class="gl-mb-5" data-testid="expandButton" @click="handleToggle"> - {{ $options.i18n.expand }} - </gl-button> + <div v-show="isCollapsed" class="nothing-here-block diff-collapsed"> + {{ __('This diff is collapsed.') }} + <a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{ + __('Click to expand it.') + }}</a> </div> <diff-content v-show="!isCollapsed && !isFileTooLarge" diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js deleted file mode 100644 index 8b91543587c..00000000000 --- a/app/assets/javascripts/diffs/i18n.js +++ /dev/null @@ -1,14 +0,0 @@ -import { __ } from '~/locale'; - -export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!'); - -export const DIFF_FILE = { - blobView: __('You can %{linkStart}view the blob%{linkEnd} instead.'), - editInFork: __( - "You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.", - ), - fork: __('Fork'), - cancel: __('Cancel'), - collapsed: __('This file is collapsed.'), - expand: __('Expand file'), -}; diff --git a/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue new file mode 100644 index 00000000000..890381a8f29 --- /dev/null +++ b/app/assets/javascripts/integrations/edit/components/confirmation_modal.vue @@ -0,0 +1,60 @@ +<script> +import { mapGetters } from 'vuex'; +import { GlModal } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlModal, + }, + computed: { + ...mapGetters(['isSavingOrTesting']), + primaryProps() { + return { + text: __('Save'), + attributes: [ + { variant: 'success' }, + { category: 'primary' }, + { disabled: this.isSavingOrTesting }, + ], + }; + }, + cancelProps() { + return { + text: __('Cancel'), + }; + }, + }, + methods: { + onSubmit() { + this.$emit('submit'); + }, + }, +}; +</script> + +<template> + <gl-modal + modal-id="confirmSaveIntegration" + size="sm" + :title="s__('Integrations|Save settings?')" + :action-primary="primaryProps" + :action-cancel="cancelProps" + @primary="onSubmit" + > + <p> + {{ + s__( + 'Integrations|Saving will update the default settings for all projects that are not using custom settings.', + ) + }} + </p> + <p class="gl-mb-0"> + {{ + s__( + 'Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults.', + ) + }} + </p> + </gl-modal> +</template> diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index 0460ed6791e..0fd39c5635d 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -1,8 +1,9 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlModalDirective } from '@gitlab/ui'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../event_hub'; +import { integrationLevels } from '../constants'; import OverrideDropdown from './override_dropdown.vue'; import ActiveCheckbox from './active_checkbox.vue'; @@ -10,6 +11,7 @@ import JiraTriggerFields from './jira_trigger_fields.vue'; import JiraIssuesFields from './jira_issues_fields.vue'; import TriggerFields from './trigger_fields.vue'; import DynamicField from './dynamic_field.vue'; +import ConfirmationModal from './confirmation_modal.vue'; export default { name: 'IntegrationForm', @@ -20,8 +22,12 @@ export default { JiraIssuesFields, TriggerFields, DynamicField, + ConfirmationModal, GlButton, }, + directives: { + 'gl-modal': GlModalDirective, + }, mixins: [glFeatureFlagsMixin()], computed: { ...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']), @@ -32,6 +38,9 @@ export default { isJira() { return this.propsSource.type === 'jira'; }, + isInstanceLevel() { + return this.propsSource.integrationLevel === integrationLevels.INSTANCE; + }, showJiraIssuesFields() { return this.isJira && this.glFeatures.jiraIssuesIntegration; }, @@ -82,7 +91,21 @@ export default { v-bind="propsSource.jiraIssuesProps" /> <div v-if="isEditable" class="footer-block row-content-block"> + <template v-if="isInstanceLevel"> + <gl-button + v-gl-modal.confirmSaveIntegration + category="primary" + variant="success" + :loading="isSaving" + :disabled="isSavingOrTesting" + data-qa-selector="save_changes_button" + > + {{ __('Save changes') }} + </gl-button> + <confirmation-modal @submit="onSaveClick" /> + </template> <gl-button + v-else category="primary" variant="success" type="submit" @@ -93,6 +116,7 @@ export default { > {{ __('Save changes') }} </gl-button> + <gl-button v-if="propsSource.canTest" :loading="isTesting" diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index b7cf39db00c..52fa0038fbb 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -396,10 +396,6 @@ export default class MergeRequestTabs { initChangesDropdown(this.stickyTop); - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - localTimeAgo($('.js-timeago', 'div#diffs')); syntaxHighlight($('#diffs .js-syntax-highlight')); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 340fbe4d887..37bb79defd1 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -479,11 +479,6 @@ export default class Notes { row = form; } - const lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; - const 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}"]`); if (!discussionContainer.length) { @@ -519,12 +514,6 @@ export default class Notes { Notes.animateAppendNote(noteEntity.html, discussionContainer); } - if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { - gl.diffNotesCompileComponents(); - - this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); - } - localTimeAgo($('.js-timeago'), false); Notes.checkMergeRequestStatus(); return this.updateNotesCount(1); @@ -538,19 +527,6 @@ export default class Notes { .get(0); } - renderDiscussionAvatar(diffAvatarContainer, noteEntity) { - let avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); - - if (!avatarHolder.length) { - avatarHolder = document.createElement('diff-note-avatars'); - avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id); - - diffAvatarContainer.append(avatarHolder); - - gl.diffNotesCompileComponents(); - } - } - /** * Called in response the main target form has been successfully submitted. * @@ -605,10 +581,6 @@ 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(); this.parentTimeline = form.parents('.timeline'); if (form.length) { @@ -714,10 +686,6 @@ export default class Notes { $note_li.replaceWith($noteEntityEl); this.setupNewNote($noteEntityEl); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } } checkContentToAllowEditing($el) { @@ -844,12 +812,6 @@ export default class Notes { const $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 @@ -979,13 +941,6 @@ export default class Notes { form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form'); - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - const $commentBtn = form.find('comment-and-resolve-btn'); - $commentBtn.attr(':discussion-id', `'${discussionID}'`); - - gl.diffNotesCompileComponents(); - } - form.find('.js-note-text').focus(); form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID); } diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue index 7a3923dfefd..a138a3a3425 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue @@ -1,11 +1,8 @@ <script> -/* eslint-disable vue/no-v-html */ import Vue from 'vue'; import Cookies from 'js-cookie'; import { GlIcon } from '@gitlab/ui'; import Translate from '../../../../../vue_shared/translate'; -// Full path is needed for Jest to be able to correctly mock this file -import illustrationSvg from '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg'; import { parseBoolean } from '~/lib/utils/common_utils'; Vue.use(Translate); @@ -20,12 +17,10 @@ export default { data() { return { docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, + imageUrl: document.getElementById('pipeline-schedules-callout').dataset.imageUrl, calloutDismissed: parseBoolean(Cookies.get(cookieKey)), }; }, - created() { - this.illustrationSvg = illustrationSvg; - }, methods: { dismissCallout() { this.calloutDismissed = true; @@ -40,7 +35,9 @@ export default { <button id="dismiss-callout-btn" class="btn btn-default close" @click="dismissCallout"> <gl-icon name="close" aria-hidden="true" /> </button> - <div class="svg-container" v-html="illustrationSvg"></div> + <div class="svg-container"> + <img :src="imageUrl" /> + </div> <div class="user-callout-copy"> <h4>{{ __('Scheduling Pipelines') }}</h4> <p> diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 586d1e62c2f..5fa6cef7195 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -57,16 +57,10 @@ export default class SingleFileDiff { this.content.hide(); this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down'); this.collapsedContent.show(); - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } } else if (this.content) { this.collapsedContent.hide(); this.content.show(); this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } } else { this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); return this.getContentHTML(cb); @@ -90,10 +84,6 @@ export default class SingleFileDiff { } this.collapsedContent.after(this.content); - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - const $file = $(this.file); FilesCommentButton.init($file); diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index b70f093e930..91a0ac3aa92 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -9,6 +9,12 @@ const fileExtensionIcons = { 'md.rendered': 'markdown', markdown: 'markdown', 'markdown.rendered': 'markdown', + mdown: 'markdown', + 'mdown.rendered': 'markdown', + mkd: 'markdown', + 'mkd.rendered': 'markdown', + mkdn: 'markdown', + 'mkdn.rendered': 'markdown', rst: 'markdown', blink: 'blink', css: 'css', diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 04167cbee1b..d7b4db3840e 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -123,20 +123,13 @@ } .build-header { - .ci-header-container, - .header-action-buttons { - display: flex; - } - - .ci-header-container { - min-height: 54px; - } - .page-content-header { padding: 10px 0 9px; } .header-action-buttons { + display: flex; + @include media-breakpoint-down(xs) { .sidebar-toggle-btn { margin-top: 0; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 40530acf1d2..10ab32d1e9a 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1013,31 +1013,35 @@ button.mini-pipeline-graph-dropdown-toggle { /** * Terminal */ -.terminal-icon { - margin-left: 3px; -} - -.terminal-container { - .content-block { - border-bottom: 0; - } +[data-page='projects:jobs:terminal'], +[data-page='projects:environments:terminal'] { + .terminal-container { + .content-block { + border-bottom: 0; + } - #terminal { - margin-top: 10px; - min-height: 450px; - box-sizing: border-box; + #terminal { + margin-top: 10px; - > div { - min-height: 450px; + > div { + min-height: 450px; + } } } } -.ci-header-container { - min-height: 55px; - - .text-center { - padding-top: 12px; +/** + * Pipelines / Jobs header + */ +[data-page='projects:pipelines:show'], +[data-page='projects:jobs:show'] { + .ci-header-container { + min-height: $gl-spacing-scale-7; + display: flex; + + .text-center { + padding-top: 12px; + } } } diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index efbb539c261..af5de684f32 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -13,13 +13,14 @@ class InvitesController < ApplicationController respond_to :html def show - track_experiment('opened') + track_new_user_invite_experiment('opened') accept if skip_invitation_prompt? end def accept if member.accept_invite!(current_user) - track_experiment('accepted') + track_new_user_invite_experiment('accepted') + track_invitation_reminders_experiment('accepted') redirect_to invite_details[:path], notice: _("You have been granted %{member_human_access} access to %{title} %{name}.") % { member_human_access: member.human_access, title: invite_details[:title], name: invite_details[:name] } else @@ -105,13 +106,25 @@ class InvitesController < ApplicationController end end - def track_experiment(action) + def track_new_user_invite_experiment(action) return unless params[:new_user_invite] property = params[:new_user_invite] == 'experiment' ? 'experiment_group' : 'control_group' + track_experiment(:invite_email, action, property) + end + + def track_invitation_reminders_experiment(action) + return unless Gitlab::Experimentation.enabled?(:invitation_reminders) + + property = Gitlab::Experimentation.enabled_for_attribute?(:invitation_reminders, member.invite_email) ? 'experimental_group' : 'control_group' + + track_experiment(:invitation_reminders, action, property) + end + + def track_experiment(experiment_key, action, property) Gitlab::Tracking.event( - Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], + Gitlab::Experimentation.experiment(experiment_key).tracking_category, action, property: property, label: Digest::MD5.hexdigest(member.to_global_id.to_s) diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 3a13c5949bd..376b1a723c3 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -77,6 +77,15 @@ module Emails Gitlab::Tracking.event(Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], 'sent', property: 'control_group') end end + + if member.invite_to_unknown_user? && Gitlab::Experimentation.enabled?(:invitation_reminders) + Gitlab::Tracking.event( + Gitlab::Experimentation.experiment(:invitation_reminders).tracking_category, + 'sent', + property: Gitlab::Experimentation.enabled_for_attribute?(:invitation_reminders, member.invite_email) ? 'experimental_group' : 'control_group', + label: Digest::MD5.hexdigest(member.to_global_id.to_s) + ) + end end def member_invite_accepted_email(member_source_type, member_id) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 7e9e24b2418..f9a55fa9157 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -42,6 +42,7 @@ module Ci has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :latest_statuses_ordered_by_stage, -> { latest.order(:stage_idx, :stage) }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline + has_many :latest_statuses, -> { latest }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline @@ -577,11 +578,11 @@ module Ci end def retried - @retried ||= (statuses.order(id: :desc) - statuses.latest) + @retried ||= (statuses.order(id: :desc) - latest_statuses) end def coverage - coverage_array = statuses.latest.map(&:coverage).compact + coverage_array = latest_statuses.map(&:coverage).compact if coverage_array.size >= 1 '%.2f' % (coverage_array.reduce(:+) / coverage_array.size) end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 1de7c90e50d..b85a902d58b 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -102,7 +102,7 @@ module Clusters def terminals(environment, data) pods = filter_by_project_environment(data[:pods], environment.project.full_path_slug, environment.slug) terminals = pods.flat_map { |pod| terminals_for_pod(api_url, environment.deployment_namespace, pod) }.compact - terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) } + terminals.each { |terminal| add_terminal_auth(terminal, **terminal_auth) } end def kubeclient diff --git a/app/models/member.rb b/app/models/member.rb index 0f9950b6e42..7c9586aef76 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -78,7 +78,10 @@ class Member < ApplicationRecord scope :request, -> { where.not(requested_at: nil) } scope :non_request, -> { where(requested_at: nil) } - scope :not_accepted_invitations_by_user, -> (user) { invite.where(invite_accepted_at: nil, created_by: user) } + scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) } + scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) } + scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) } + scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) } scope :has_access, -> { active.where('access_level > 0') } diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb index 002be8be729..f4d81652390 100644 --- a/app/serializers/merge_request_poll_cached_widget_entity.rb +++ b/app/serializers/merge_request_poll_cached_widget_entity.rb @@ -15,7 +15,7 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity expose :target_project_id expose :squash expose :rebase_in_progress?, as: :rebase_in_progress - expose :default_squash_commit_message, if: -> (merge_request, _) { merge_request.mergeable? } + expose :default_squash_commit_message expose :commits_count expose :merge_ongoing?, as: :merge_ongoing expose :work_in_progress?, as: :work_in_progress @@ -25,10 +25,10 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity expose :source_branch_exists?, as: :source_branch_exists expose :branch_missing?, as: :branch_missing - expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity, - if: -> (merge_request, _) { merge_request.mergeable? } do |merge_request| + expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity do |merge_request| merge_request.recent_commits.without_merge_commits end + expose :diff_head_sha do |merge_request| merge_request.diff_head_sha.presence end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 45c5a1d3e1c..a45214670fa 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -47,6 +47,7 @@ class PipelineSerializer < BaseSerializer :retryable_builds, :scheduled_actions, :stages, + :latest_statuses, :trigger_requests, :user, { @@ -62,7 +63,14 @@ class PipelineSerializer < BaseSerializer pending_builds: :project, project: [:route, { namespace: :route }], triggered_by_pipeline: [{ project: [:route, { namespace: :route }] }, :user], - triggered_pipelines: [{ project: [:route, { namespace: :route }] }, :user, :source_job] + triggered_pipelines: [ + { + project: [:route, { namespace: :route }] + }, + :source_job, + :latest_statuses, + :user + ] } ] end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 18bae26613f..e511e26adfe 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -31,14 +31,14 @@ module Ci # rubocop: disable CodeReuse/ActiveRecord def update_retried # find the latest builds for each name - latest_statuses = pipeline.statuses.latest + latest_statuses = pipeline.latest_statuses .group(:name) .having('count(*) > 1') .pluck(Arel.sql('MAX(id)'), 'name') # mark builds that are retried if latest_statuses.any? - pipeline.statuses.latest + pipeline.latest_statuses .where(name: latest_statuses.map(&:second)) .where.not(id: latest_statuses.map(&:first)) .update_all(retried: true) diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml deleted file mode 100644 index 594cdc7e2f2..00000000000 --- a/app/views/discussions/_jump_to_next.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- discussion = local_assigns.fetch(:discussion, nil) -- if current_user - %jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" } - .btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" } - %button.btn.gl-button.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion", - ":title" => "buttonText", - ":aria-label" => "buttonText", - data: { container: "body" } } - = custom_icon("next_discussion") diff --git a/app/views/discussions/_new_issue_for_all_discussions.html.haml b/app/views/discussions/_new_issue_for_all_discussions.html.haml deleted file mode 100644 index 7e3863b691c..00000000000 --- a/app/views/discussions/_new_issue_for_all_discussions.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project) - .btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" } - = link_to custom_icon('icon_mr_issue'), - new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid), - title: 'Resolve all discussions in new issue', - aria: { label: 'Resolve all discussions in new issue' }, - data: { container: 'body' }, - class: 'new-issue-for-discussion btn gl-button btn-default discussion-create-issue-btn has-tooltip' diff --git a/app/views/discussions/_new_issue_for_discussion.html.haml b/app/views/discussions/_new_issue_for_discussion.html.haml deleted file mode 100644 index e4051ea714a..00000000000 --- a/app/views/discussions/_new_issue_for_discussion.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -- if discussion.can_resolve?(current_user) && can?(current_user, :create_issue, @project) - %new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'", - "inline-template" => true } - .btn-group{ role: "group", "v-if" => "showButton" } - = link_to custom_icon('icon_mr_issue'), - new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id), - title: 'Resolve this thread in a new issue', - aria: { label: 'Resolve this thread in a new issue' }, - data: { container: 'body' }, - class: 'new-issue-for-discussion btn gl-button btn-default discussion-create-issue-btn has-tooltip' diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index cdc6c602277..7db318f83b1 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -21,22 +21,8 @@ - if can_create_note? %a.user-avatar-link.d-none.d-sm-block{ href: user_path(current_user) } = image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40' - - if discussion.potentially_resolvable? - - line_type = local_assigns.fetch(:line_type, nil) - - .discussion-with-resolve-btn - .btn-group.discussion-with-resolve-btn{ role: "group" } - .btn-group{ role: "group" } - = link_to_reply_discussion(discussion, line_type) - - = render "discussions/resolve_all", discussion: discussion - - .btn-group.discussion-actions - = render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable - = render "discussions/jump_to_next", discussion: discussion - - else - .discussion-with-resolve-btn - = link_to_reply_discussion(discussion) + .discussion-with-resolve-btn + = link_to_reply_discussion(discussion) - elsif !current_user .disabled-comment.text-center Please diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index a6c9f4fc0f8..3e85e1f5b90 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -55,12 +55,13 @@ - unless use_startup_css? = stylesheet_link_tag_defer "themes/#{user_application_theme_css_filename}" if user_application_theme_css_filename = stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations'] - = stylesheet_link_tag_defer 'performance_bar' if performance_bar_enabled? = stylesheet_link_tag_defer "highlight/themes/#{user_color_scheme}" = render 'layouts/startup_css_activation' + = stylesheet_link_tag 'performance_bar' if performance_bar_enabled? + = Gon::Base.render_data(nonce: content_security_policy_nonce) - if content_for?(:library_javascripts) diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml index 65a2f75a3e2..72bcfbdf3af 100644 --- a/app/views/notify/autodevops_disabled_email.html.haml +++ b/app/views/notify/autodevops_disabled_email.html.haml @@ -46,4 +46,4 @@ %td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" } API -= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed += render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.latest_statuses.failed diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb index f849c017265..c75857e96d7 100644 --- a/app/views/notify/autodevops_disabled_email.text.erb +++ b/app/views/notify/autodevops_disabled_email.text.erb @@ -7,7 +7,7 @@ The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_ <% else -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API <% end -%> -<% failed = @pipeline.statuses.latest.failed -%> +<% failed = @pipeline.latest_statuses.failed -%> had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. <% failed.each do |build| -%> diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index f01181857ce..575ec8c488e 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -108,4 +108,4 @@ %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" } API -= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed += render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.latest_statuses.failed diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb index b388aad7048..a30e331d892 100644 --- a/app/views/notify/pipeline_failed_email.text.erb +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -27,7 +27,7 @@ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <% <% else -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API <% end -%> -<% failed = @pipeline.statuses.latest.failed -%> +<% failed = @pipeline.latest_statuses.failed -%> had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. <% failed.each do |build| -%> diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index d35443cca1e..4d40071e07c 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -21,9 +21,6 @@ - else = add_diff_note_button(line_code, diff_file.position(line), type) %a{ href: "##{line_code}", data: { linenumber: link_text } } - - discussion = line_discussions.try(:first) - - if discussion && discussion.resolvable? && !plain - %diff-note-avatars{ "discussion-id" => discussion.id } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } } - link_text = type == "old" ? " " : line.new_pos - if plain diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 9587ea4696b..ebe3aad064a 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -20,9 +20,6 @@ %td.old_line.diff-line-num.js-avatar-container{ class: left.type, data: { linenumber: left.old_pos } } = add_diff_note_button(left_line_code, left_position, 'old') %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } - - discussion_left = discussions_left.try(:first) - - if discussion_left && discussion_left.resolvable? - %diff-note-avatars{ "discussion-id" => discussion_left.id } %td.line_content.parallel.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.rich_text) - else %td.old_line.diff-line-num.empty-cell @@ -41,9 +38,6 @@ %td.new_line.diff-line-num.js-avatar-container{ class: right.type, data: { linenumber: right.new_pos } } = add_diff_note_button(right_line_code, right_position, 'new') %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } - - discussion_right = discussions_right.try(:first) - - if discussion_right && discussion_right.resolvable? - %diff-note-avatars{ "discussion-id" => discussion_right.id } %td.line_content.parallel.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.rich_text) - else %td.old_line.diff-line-num.empty-cell diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml deleted file mode 100644 index ecb51aca847..00000000000 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- content_for :note_actions do - - if can?(current_user, :update_merge_request, @merge_request) - - if @merge_request.open? - = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"} - - if @merge_request.reopenable? - = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} - %comment-and-resolve-btn{ "inline-template" => true } - %button.btn.btn-nr.btn-default.gl-mr-3.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } } - {{ buttonText }} - -#notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 2b2b79d886b..0bcf8ae605a 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -2,7 +2,7 @@ - page_title _("Pipeline Schedules") -#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules') } } +#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), image_url: image_path('pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg') } } .top-area - schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) } = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope diff --git a/app/views/shared/icons/_next_discussion.svg b/app/views/shared/icons/_next_discussion.svg deleted file mode 100644 index 43559a60cb0..00000000000 --- a/app/views/shared/icons/_next_discussion.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="0 0 20 19" ><path d="M15.21 7.783h-3.317c-.268 0-.472.218-.472.486v.953c0 .28.212.486.473.486h3.318v1.575c0 .36.233.452.52.23l3.06-2.37c.274-.213.286-.582 0-.804l-3.06-2.37c-.275-.213-.52-.12-.52.23v1.583zm.57-3.66c-1.558-1.22-3.783-1.98-6.254-1.98C4.816 2.143 1 4.91 1 8.333c0 1.964 1.256 3.715 3.216 4.846-.447 1.615-1.132 2.195-1.732 2.882-.142.174-.304.32-.256.56v.01c.047.213.218.368.41.368h.046c.37-.048.743-.116 1.085-.213 1.645-.425 3.13-1.22 4.377-2.34.447.048.913.077 1.38.077 2.092 0 4.01-.546 5.492-1.454-.416-.208-.798-.475-1.134-.792-1.227.63-2.743 1.008-4.36 1.008-.41 0-.828-.03-1.237-.078l-.543-.058-.41.368c-.78.696-1.655 1.248-2.616 1.654.248-.445.486-.977.667-1.664l.257-.928-.828-.484c-1.646-.948-2.598-2.32-2.598-3.763 0-2.69 3.35-4.952 7.308-4.952 1.893 0 3.647.518 4.962 1.353.393-.266.827-.473 1.29-.61z" /></svg> |