summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-09 21:08:33 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-09 21:08:33 +0000
commitb296ffa543e23f57fa2692539e6f0028c59e2203 (patch)
treef5224a37cac088506d1c8fd53925ee1d9fd2a02c /app
parentc172bb9967f280e05bd904188d60a959dff10f00 (diff)
downloadgitlab-ce-b296ffa543e23f57fa2692539e6f0028c59e2203.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue19
-rw-r--r--app/assets/javascripts/behaviors/index.js22
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js4
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue21
-rw-r--r--app/assets/javascripts/boards/stores/actions.js8
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js7
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js18
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue27
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue23
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue21
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue8
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue4
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js15
-rw-r--r--app/assets/javascripts/header.js77
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue1
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/performance_bar/index.js8
-rw-r--r--app/assets/javascripts/set_status_modal/event_hub.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue27
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/alert_details_table.vue47
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue4
-rw-r--r--app/assets/stylesheets/framework/variables.scss26
-rw-r--r--app/controllers/projects/issues_controller.rb15
-rw-r--r--app/graphql/types/current_user_todos.rb24
-rw-r--r--app/graphql/types/design_management/design_type.rb1
-rw-r--r--app/graphql/types/issue_type.rb1
-rw-r--r--app/graphql/types/merge_request_type.rb1
-rw-r--r--app/graphql/types/todo_type.rb2
-rw-r--r--app/models/concerns/has_wiki.rb4
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_services/chat_message/merge_message.rb16
-rw-r--r--app/models/project_wiki.rb17
-rw-r--r--app/models/wiki.rb24
-rw-r--r--app/services/git/wiki_push_service.rb21
-rw-r--r--app/services/git/wiki_push_service/change.rb6
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml6
-rw-r--r--app/workers/post_receive.rb17
44 files changed, 361 insertions, 213 deletions
diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue
index c1ebd234088..c6605452616 100644
--- a/app/assets/javascripts/alert_management/components/alert_details.vue
+++ b/app/assets/javascripts/alert_management/components/alert_details.vue
@@ -10,7 +10,6 @@ import {
GlTabs,
GlTab,
GlButton,
- GlTable,
} from '@gitlab/ui';
import { s__ } from '~/locale';
import alertQuery from '../graphql/queries/details.query.graphql';
@@ -28,6 +27,7 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils';
import SystemNote from './system_notes/system_note.vue';
import AlertSidebar from './alert_sidebar.vue';
import AlertMetrics from './alert_metrics.vue';
+import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
const containerEl = document.querySelector('.page-with-contextual-sidebar');
@@ -55,6 +55,7 @@ export default {
},
],
components: {
+ AlertDetailsTable,
GlBadge,
GlAlert,
GlIcon,
@@ -63,7 +64,6 @@ export default {
GlTab,
GlTabs,
GlButton,
- GlTable,
TimeAgoTooltip,
AlertSidebar,
SystemNote,
@@ -331,20 +331,7 @@ export default {
</div>
<div class="gl-pl-2" data-testid="runbook">{{ alert.runbook }}</div>
</div>
- <gl-table
- class="alert-management-details-table"
- :items="[{ 'Full Alert Payload': 'Value', ...alert }]"
- :show-empty="true"
- :busy="loading"
- stacked
- >
- <template #empty>
- {{ s__('AlertManagement|No alert data to display.') }}
- </template>
- <template #table-busy>
- <gl-loading-icon size="lg" color="dark" class="mt-3" />
- </template>
- </gl-table>
+ <alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
<gl-tab :data-testid="$options.tabsConfig[1].id" :title="$options.tabsConfig[1].title">
<alert-metrics :dashboard-url="alert.metricsDashboardUrl" />
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 8060938c72a..fd12c282b62 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,7 +1,7 @@
+import $ from 'jquery';
import './autosize';
import './bind_in_out';
import './markdown/render_gfm';
-import initGFMInput from './markdown/gfm_auto_complete';
import initCopyAsGFM from './markdown/copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior';
@@ -15,9 +15,27 @@ import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resi
import initSelect2Dropdowns from './select2';
installGlEmojiElement();
-initGFMInput();
+
initCopyAsGFM();
initCopyToClipboard();
+
initPageShortcuts();
initCollapseSidebarOnWindowResize();
initSelect2Dropdowns();
+
+document.addEventListener('DOMContentLoaded', () => {
+ window.requestIdleCallback(
+ () => {
+ // Check if we have to Load GFM Input
+ const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)');
+ if ($gfmInputs.length) {
+ import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete')
+ .then(({ default: initGFMInput }) => {
+ initGFMInput($gfmInputs);
+ })
+ .catch(() => {});
+ }
+ },
+ { timeout: 500 },
+ );
+});
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index 6bbd2133344..83f2ca0bdc2 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -2,8 +2,8 @@ import $ from 'jquery';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { parseBoolean } from '~/lib/utils/common_utils';
-export default function initGFMInput() {
- $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
+export default function initGFMInput($els) {
+ $els.each((i, el) => {
const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
const enableGFM = parseBoolean(el.dataset.supportsAutocomplete);
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index 9251af01aff..06f436adb8e 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlPopover, GlSprintf, GlButton } from '@gitlab/ui';
import { parseBoolean, scrollToElement, setCookie, getCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
@@ -114,7 +113,7 @@ export default {
:css-classes="['suggest-gitlab-ci-yml', 'ml-4']"
>
<template #title>
- <span v-html="suggestTitle"></span>
+ <span>{{ suggestTitle }}</span>
<span class="ml-auto">
<gl-button
:aria-label="__('Close')"
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 34e8438ba4c..2817f9cb13d 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,11 +1,13 @@
<script>
import $ from 'jquery';
+import { mapActions, mapGetters } from 'vuex';
import { GlButton } from '@gitlab/ui';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import ListIssue from 'ee_else_ce/boards/models/issue';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
import boardsStore from '../stores/boards_store';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'BoardNewIssue',
@@ -13,6 +15,7 @@ export default {
ProjectSelect,
GlButton,
},
+ mixins: [glFeatureFlagMixin()],
props: {
groupId: {
type: Number,
@@ -32,6 +35,7 @@ export default {
};
},
computed: {
+ ...mapGetters(['isSwimlanesOn']),
disabled() {
if (this.groupId) {
return this.title === '' || !this.selectedProject.name;
@@ -44,6 +48,7 @@ export default {
eventHub.$on('setSelectedProject', this.setSelectedProject);
},
methods: {
+ ...mapActions(['addListIssue', 'addListIssueFailure']),
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return Promise.resolve();
@@ -70,21 +75,31 @@ export default {
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
+ if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) {
+ this.addListIssue({ list: this.list, issue, position: 0 });
+ }
+
return this.list
.newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
- boardsStore.setIssueDetail(issue);
- boardsStore.setListDetail(this.list);
+ if (!this.glFeatures.boardsWithSwimlanes || !this.isSwimlanesOn) {
+ boardsStore.setIssueDetail(issue);
+ boardsStore.setListDetail(this.list);
+ }
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
// Remove the issue
- this.list.removeIssue(issue);
+ if (this.glFeatures.boardsWithSwimlanes && this.isSwimlanesOn) {
+ this.addListIssueFailure({ list: this.list, issue });
+ } else {
+ this.list.removeIssue(issue);
+ }
// Show error message
this.error = true;
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 4e808c809fb..8d9b58f71cb 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -235,6 +235,14 @@ export default {
notImplemented();
},
+ addListIssue: ({ commit }, { list, issue, position }) => {
+ commit(types.ADD_ISSUE_TO_LIST, { list, issue, position });
+ },
+
+ addListIssueFailure: ({ commit }, { list, issue }) => {
+ commit(types.ADD_ISSUE_TO_LIST_FAILURE, { list, issue });
+ },
+
fetchBacklog: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index f336d6d03c9..4fdbfbc36c5 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -15,6 +15,7 @@ import {
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import eventHub from '../eventhub';
import { ListType } from '../constants';
import IssueProject from '../models/project';
@@ -303,7 +304,7 @@ const boardsStore = {
onNewListIssueResponse(list, issue, data) {
issue.refreshData(data);
- if (list.issuesSize > 1) {
+ if (!gon.features.boardsWithSwimlanes && list.issuesSize > 1) {
const moveBeforeId = list.issues[1].id;
this.moveIssue(issue.id, null, null, null, moveBeforeId);
}
@@ -710,6 +711,10 @@ const boardsStore = {
},
newIssue(id, issue) {
+ if (typeof id === 'string') {
+ id = getIdFromGraphQLId(id);
+ }
+
return axios.post(this.generateIssuesPath(id), {
issue,
});
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 12dd96380f6..a3b84108cb3 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -24,6 +24,8 @@ export const RECEIVE_MOVE_ISSUE_ERROR = 'RECEIVE_MOVE_ISSUE_ERROR';
export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE';
export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS';
export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR';
+export const ADD_ISSUE_TO_LIST = 'ADD_ISSUE_TO_LIST';
+export const ADD_ISSUE_TO_LIST_FAILURE = 'ADD_ISSUE_TO_LIST_FAILURE';
export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 837bccce091..f25c339836f 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { sortBy } from 'lodash';
+import { sortBy, pull } from 'lodash';
import * as mutationTypes from './mutation_types';
import { __ } from '~/locale';
@@ -8,6 +8,10 @@ const notImplemented = () => {
throw new Error('Not implemented!');
};
+const removeIssueFromList = (state, listId, issueId) => {
+ Vue.set(state.issuesByListId, listId, pull(state.issuesByListId[listId], issueId));
+};
+
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, showPromotion, ...endpoints } = data;
@@ -131,6 +135,18 @@ export default {
notImplemented();
},
+ [mutationTypes.ADD_ISSUE_TO_LIST]: (state, { list, issue, position }) => {
+ const listIssues = state.issuesByListId[list.id];
+ listIssues.splice(position, 0, issue.id);
+ Vue.set(state.issuesByListId, list.id, listIssues);
+ Vue.set(state.issues, issue.id, issue);
+ },
+
+ [mutationTypes.ADD_ISSUE_TO_LIST_FAILURE]: (state, { list, issue }) => {
+ state.error = __('An error occurred while creating the issue. Please try again.');
+ removeIssueFromList(state, list.id, issue.id);
+ },
+
[mutationTypes.SET_CURRENT_PAGE]: () => {
notImplemented();
},
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index f87bd695560..845f1aec8cf 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,6 +1,6 @@
<script>
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLoadingIcon, GlLink, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
@@ -27,6 +27,7 @@ export default {
GlLink,
ToggleRepliesWidget,
TimeAgoTooltip,
+ GlBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -148,14 +149,14 @@ export default {
}
},
onCreateNoteError(err) {
- this.$emit('createNoteError', err);
+ this.$emit('create-note-error', err);
},
hideForm() {
this.isFormRendered = false;
this.discussionComment = '';
},
showForm() {
- this.$emit('openForm', this.discussion.id);
+ this.$emit('open-form', this.discussion.id);
this.isFormRendered = true;
},
toggleResolvedStatus() {
@@ -167,11 +168,11 @@ export default {
})
.then(({ data }) => {
if (data.errors?.length > 0) {
- this.$emit('resolveDiscussionError', data.errors[0]);
+ this.$emit('resolve-discussion-error', data.errors[0]);
}
})
.catch(err => {
- this.$emit('resolveDiscussionError', err);
+ this.$emit('resolve-discussion-error', err);
})
.finally(() => {
this.isResolving = false;
@@ -192,13 +193,12 @@ export default {
<template>
<div class="design-discussion-wrapper">
- <div
- class="badge badge-pill gl-display-flex gl-align-items-center gl-justify-content-center"
+ <gl-badge
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-cursor-pointer"
:class="{ resolved: discussion.resolved }"
- type="button"
>
{{ discussion.index }}
- </div>
+ </gl-badge>
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
data-qa-selector="design_discussion_content"
@@ -208,7 +208,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
- @error="$emit('updateNoteError', $event)"
+ @error="$emit('update-note-error', $event)"
>
<template v-if="discussion.resolvable" #resolveDiscussion>
<button
@@ -216,7 +216,6 @@ export default {
:class="{ 'is-active': discussion.resolved }"
:title="resolveCheckboxText"
:aria-label="resolveCheckboxText"
- type="button"
class="line-resolve-btn note-action-button gl-mr-3"
data-testid="resolve-button"
@click.stop="toggleResolvedStatus"
@@ -252,7 +251,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
- @error="$emit('updateNoteError', $event)"
+ @error="$emit('update-note-error', $event)"
/>
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
<reply-placeholder
@@ -275,8 +274,8 @@ export default {
v-model="discussionComment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
>
<template v-if="discussion.resolvable" #resolveCheckbox>
<label data-testid="resolve-checkbox">
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index 6c380153a3f..18444a2cc2f 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -1,7 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
import { ApolloMutation } from 'vue-apollo';
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
import updateNoteMutation from '../../graphql/mutations/update_note.mutation.graphql';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -18,6 +18,7 @@ export default {
DesignReplyForm,
ApolloMutation,
GlIcon,
+ GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -83,27 +84,27 @@ export default {
:img-alt="author.username"
:img-size="40"
/>
- <div class="d-flex justify-content-between">
+ <div class="gl-display-flex gl-justify-content-space-between">
<div>
- <a
+ <gl-link
v-once
:href="author.webUrl"
class="js-user-link"
:data-user-id="author.id"
:data-username="author.username"
>
- <span class="note-header-author-name bold">{{ author.name }}</span>
+ <span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span>
<span v-if="author.status_tooltip_html" v-html="author.status_tooltip_html"></span>
<span class="note-headline-light">@{{ author.username }}</span>
- </a>
+ </gl-link>
<span class="note-headline-light note-headline-meta">
<span class="system-note-message"> <slot></slot> </span>
- <a
+ <gl-link
class="note-timestamp system-note-separator gl-display-block gl-mb-2"
:href="`#note_${noteAnchorId}`"
>
<time-ago-tooltip :time="note.createdAt" tooltip-placement="bottom" />
- </a>
+ </gl-link>
</span>
</div>
<div class="gl-display-flex">
@@ -122,7 +123,7 @@ export default {
</div>
<template v-if="!isEditing">
<div
- class="note-text js-note-text md"
+ class="note-text js-note-text"
data-qa-selector="note_content"
v-html="note.bodyHtml"
></div>
@@ -143,9 +144,9 @@ export default {
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
:is-new-comment="false"
- class="mt-5"
- @submitForm="mutate"
- @cancelForm="hideForm"
+ class="gl-mt-5"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
/>
</apollo-mutation>
</timeline-entry-item>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 969034909f2..3754e1dbbc1 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDeprecatedButton, GlModal } from '@gitlab/ui';
+import { GlButton, GlModal } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { s__ } from '~/locale';
@@ -7,7 +7,7 @@ export default {
name: 'DesignReplyForm',
components: {
MarkdownField,
- GlDeprecatedButton,
+ GlButton,
GlModal,
},
props: {
@@ -66,13 +66,13 @@ export default {
},
methods: {
submitForm() {
- if (this.hasValue) this.$emit('submitForm');
+ if (this.hasValue) this.$emit('submit-form');
},
cancelComment() {
if (this.hasValue && this.formText !== this.value) {
this.$refs.cancelCommentModal.show();
} else {
- this.$emit('cancelForm');
+ this.$emit('cancel-form');
}
},
focusInput() {
@@ -112,20 +112,21 @@ export default {
</markdown-field>
<slot name="resolveCheckbox"></slot>
<div class="note-form-actions gl-display-flex gl-justify-content-space-between">
- <gl-deprecated-button
+ <gl-button
ref="submitButton"
:disabled="!hasValue || isSaving"
+ category="primary"
variant="success"
type="submit"
data-track-event="click_button"
data-qa-selector="save_comment_button"
- @click="$emit('submitForm')"
+ @click="$emit('submit-form')"
>
{{ buttonText }}
- </gl-deprecated-button>
- <gl-deprecated-button ref="cancelButton" @click="cancelComment">{{
+ </gl-button>
+ <gl-button ref="cancelButton" variant="default" category="primary" @click="cancelComment">{{
__('Cancel')
- }}</gl-deprecated-button>
+ }}</gl-button>
</div>
<gl-modal
ref="cancelCommentModal"
@@ -134,7 +135,7 @@ export default {
:ok-title="modalSettings.okTitle"
:cancel-title="modalSettings.cancelTitle"
modal-id="cancel-comment-modal"
- @ok="$emit('cancelForm')"
+ @ok="$emit('cancel-form')"
>{{ modalSettings.content }}
</gl-modal>
</form>
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index 9cfd2ea43a9..df425e3b96d 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -159,11 +159,11 @@ export default {
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:discussion-with-open-form="discussionWithOpenForm"
data-testid="unresolved-discussion"
- @createNoteError="$emit('onDesignDiscussionError', $event)"
- @updateNoteError="$emit('updateNoteError', $event)"
- @resolveDiscussionError="$emit('resolveDiscussionError', $event)"
+ @create-note-error="$emit('onDesignDiscussionError', $event)"
+ @update-note-error="$emit('updateNoteError', $event)"
+ @resolve-discussion-error="$emit('resolveDiscussionError', $event)"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
- @openForm="updateDiscussionWithOpenForm"
+ @open-form="updateDiscussionWithOpenForm"
/>
<template v-if="resolvedDiscussions.length > 0">
<gl-button
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 8a9911f55a3..c6225c516e2 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -372,8 +372,8 @@ export default {
v-model="comment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
- @submitForm="mutate"
- @cancelForm="closeCommentForm"
+ @submit-form="mutate"
+ @cancel-form="closeCommentForm"
/> </apollo-mutation
></template>
</design-sidebar>
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 36c586ddfd2..d57fde59db7 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -71,12 +71,15 @@ class GfmAutoComplete {
setupLifecycle() {
this.input.each((i, input) => {
const $input = $(input);
- $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
- $input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
- // This triggers at.js again
- // Needed for quick actions with suffixes (ex: /label ~)
- $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
- $input.on('clear-commands-cache.atwho', () => this.clearCache());
+ if (!$input.hasClass('js-gfm-input-initialized')) {
+ $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+ $input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
+ // This triggers at.js again
+ // Needed for quick actions with suffixes (ex: /label ~)
+ $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
+ $input.on('clear-commands-cache.atwho', () => this.clearCache());
+ $input.addClass('js-gfm-input-initialized');
+ }
});
}
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 3f9163e924d..575d3618313 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -2,9 +2,6 @@ import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { highCountTrim } from '~/lib/utils/text_utility';
-import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue';
-import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
/**
@@ -26,51 +23,43 @@ export default function initTodoToggle() {
function initStatusTriggers() {
const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
- const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
- if (setStatusModalTriggerEl || setStatusModalWrapperEl) {
- Vue.use(Translate);
+ if (setStatusModalTriggerEl) {
+ setStatusModalTriggerEl.addEventListener('click', () => {
+ import(
+ /* webpackChunkName: 'statusModalBundle' */ './set_status_modal/set_status_modal_wrapper.vue'
+ )
+ .then(({ default: SetStatusModalWrapper }) => {
+ const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
+ const statusModalElement = document.createElement('div');
+ setStatusModalWrapperEl.appendChild(statusModalElement);
- // eslint-disable-next-line no-new
- new Vue({
- el: setStatusModalTriggerEl,
- data() {
- const { hasStatus } = this.$options.el.dataset;
+ Vue.use(Translate);
- return {
- hasStatus: parseBoolean(hasStatus),
- };
- },
- render(createElement) {
- return createElement(SetStatusModalTrigger, {
- props: {
- hasStatus: this.hasStatus,
- },
- });
- },
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el: setStatusModalWrapperEl,
- data() {
- const { currentEmoji, currentMessage } = this.$options.el.dataset;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: statusModalElement,
+ data() {
+ const { currentEmoji, currentMessage } = setStatusModalWrapperEl.dataset;
- return {
- currentEmoji,
- currentMessage,
- };
- },
- render(createElement) {
- const { currentEmoji, currentMessage } = this;
+ return {
+ currentEmoji,
+ currentMessage,
+ };
+ },
+ render(createElement) {
+ const { currentEmoji, currentMessage } = this;
- return createElement(SetStatusModalWrapper, {
- props: {
- currentEmoji,
- currentMessage,
- },
- });
- },
+ return createElement(SetStatusModalWrapper, {
+ props: {
+ currentEmoji,
+ currentMessage,
+ },
+ });
+ },
+ });
+ })
+ .catch(() => {});
});
}
}
@@ -101,5 +90,5 @@ export function initNavUserDropdownTracking() {
document.addEventListener('DOMContentLoaded', () => {
requestIdleCallback(initStatusTriggers);
- initNavUserDropdownTracking();
+ requestIdleCallback(initNavUserDropdownTracking);
});
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 0bfad0befb3..183816921c1 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -44,6 +44,7 @@ export default {
:aria-label="s__('IDE|Edit')"
data-container="body"
data-placement="right"
+ data-qa-selector="edit_mode_tab"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)"
@@ -78,8 +79,9 @@ export default {
:aria-label="s__('IDE|Commit')"
data-container="body"
data-placement="right"
+ data-qa-selector="commit_mode_tab"
type="button"
- class="ide-sidebar-link js-ide-commit-mode qa-commit-mode-tab"
+ class="ide-sidebar-link js-ide-commit-mode"
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"
>
<gl-icon name="commit" />
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 77a7151e275..146e818d654 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -103,6 +103,7 @@ export default {
:title="lastCommit.message"
:href="getCommitPath(lastCommit.short_id)"
class="commit-sha"
+ data-qa-selector="commit_sha_content"
>{{ lastCommit.short_id }}</a
>
by
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2ef0feaa2af..4e2770a24c2 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -31,7 +31,6 @@ import initLogoAnimation from './logo';
import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent';
-import initPerformanceBar from './performance_bar';
import initSearchAutocomplete from './search_autocomplete';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
@@ -164,8 +163,6 @@ document.addEventListener('DOMContentLoaded', () => {
const $document = $(document);
const bootstrapBreakpoint = bp.getBreakpointSize();
- if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' });
-
initUserTracking();
initLayoutNav();
initAlertHandler();
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 88d513f6076..cfe674f9c52 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -380,7 +380,7 @@ export default {
dir="auto"
:disabled="isSubmitting"
name="note[note]"
- class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input"
+ class="note-textarea js-vue-comment-form js-note-text js-gfm-input js-autosize markdown-area qa-comment-input"
data-supports-quick-actions="true"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 0ef2d5743b1..88b4461cf38 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -337,7 +337,7 @@ export default {
v-model="updatedNoteBody"
:data-supports-quick-actions="!isEditing"
name="note[note]"
- class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input"
+ class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form qa-reply-input"
dir="auto"
:aria-label="__('Description')"
:placeholder="__('Write a comment or drag your files here…')"
diff --git a/app/assets/javascripts/performance_bar/index.js b/app/assets/javascripts/performance_bar/index.js
index ac10d99612c..f88314dabcf 100644
--- a/app/assets/javascripts/performance_bar/index.js
+++ b/app/assets/javascripts/performance_bar/index.js
@@ -5,7 +5,7 @@ import axios from '~/lib/utils/axios_utils';
import PerformanceBarService from './services/performance_bar_service';
import PerformanceBarStore from './stores/performance_bar_store';
-export default ({ container }) =>
+const initPerformanceBar = ({ container }) =>
new Vue({
el: container,
components: {
@@ -118,3 +118,9 @@ export default ({ container }) =>
});
},
});
+
+document.addEventListener('DOMContentLoaded', () => {
+ initPerformanceBar({ container: '#js-peek' });
+});
+
+export default initPerformanceBar;
diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/set_status_modal/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
deleted file mode 100644
index 0e8b6d93f42..00000000000
--- a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import eventHub from './event_hub';
-
-export default {
- props: {
- hasStatus: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- buttonText() {
- return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status');
- },
- },
- methods: {
- openModal() {
- eventHub.$emit('openModal');
- },
- },
-};
-</script>
-
-<template>
- <button type="button" class="btn menu-item" @click="openModal">{{ buttonText }}</button>
-</template>
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index a841cca8c95..09e893ff285 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -6,7 +6,6 @@ import { GlModal, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __, s__ } from '~/locale';
import Api from '~/api';
-import eventHub from './event_hub';
import EmojiMenuInModal from './emoji_menu_in_modal';
import * as Emoji from '~/emoji';
@@ -48,15 +47,12 @@ export default {
},
},
mounted() {
- eventHub.$on('openModal', this.openModal);
+ this.$root.$emit('bv::show::modal', this.modalId);
},
beforeDestroy() {
this.emojiMenu.destroy();
},
methods: {
- openModal() {
- this.$root.$emit('bv::show::modal', this.modalId);
- },
closeModal() {
this.$root.$emit('bv::hide::modal', this.modalId);
},
diff --git a/app/assets/javascripts/vue_shared/components/alert_details_table.vue b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
new file mode 100644
index 00000000000..2cd71669ecb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/alert_details_table.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ },
+ props: {
+ alert: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ tableHeader: {
+ [s__('AlertManagement|Full Alert Payload')]: s__('AlertManagement|Value'),
+ },
+ computed: {
+ items() {
+ if (!this.alert) {
+ return [];
+ }
+ return [{ ...this.$options.tableHeader, ...this.alert }];
+ },
+ },
+};
+</script>
+<template>
+ <gl-table
+ class="alert-management-details-table"
+ :busy="loading"
+ :empty-text="s__('AlertManagement|No alert data to display.')"
+ :items="items"
+ show-empty
+ stacked
+ >
+ <template #table-busy>
+ <gl-loading-icon size="lg" color="dark" class="gl-mt-5" />
+ </template>
+ </gl-table>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index f6d0fa9d8ed..f30676e8ef3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,4 +1,6 @@
<script>
+/* eslint-disable vue/no-v-html */
+
/**
* Common component to render a system note, icon and user information.
*
@@ -106,7 +108,7 @@ export default {
:class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
class="note system-note note-wrapper"
>
- <div v-safe-html="iconHtml" class="timeline-icon"></div>
+ <div class="timeline-icon" v-html="iconHtml"></div>
<div class="timeline-content">
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d1491fb50b0..cfe62d73e5d 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -152,6 +152,18 @@ $red-800: #8d1300 !default;
$red-900: #660e00 !default;
$red-950: #4d0a00 !default;
+$purple-50: #f4f0ff !default;
+$purple-100: #e1d8f9 !default;
+$purple-200: #cbbbf2 !default;
+$purple-300: #ac93e6 !default;
+$purple-400: #9475db !default;
+$purple-500: #7b58cf !default;
+$purple-600: #694cc0 !default;
+$purple-700: #5943b6 !default;
+$purple-800: #453894 !default;
+$purple-900: #2f2a6b !default;
+$purple-950: #232150 !default;
+
$gray-10: #fafafa !default;
$gray-50: #f0f0f0 !default;
$gray-100: #dbdbdb !default;
@@ -221,6 +233,20 @@ $reds: (
'950': $red-950
);
+$purples: (
+ '50': $purple-50,
+ '100': $purple-100,
+ '200': $purple-200,
+ '300': $purple-300,
+ '400': $purple-400,
+ '500': $purple-500,
+ '600': $purple-600,
+ '700': $purple-700,
+ '800': $purple-800,
+ '900': $purple-900,
+ '950': $purple-950
+);
+
$grays: (
'10': $gray-10,
'50': $gray-50,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index c2841c254eb..af1fc870f54 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,13 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions
include RecordUserLastActivity
- def issue_except_actions
- %i[index calendar new create bulk_update import_csv export_csv service_desk]
- end
-
- def set_issuables_index_only_actions
- %i[index calendar service_desk]
- end
+ ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
+ SET_ISSUEABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) }
@@ -25,10 +20,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
- before_action :issue, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) }
- after_action :log_issue_show, unless: ->(c) { c.issue_except_actions.include?(c.action_name.to_sym) }
+ before_action :issue, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
+ after_action :log_issue_show, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
- before_action :set_issuables_index, if: ->(c) { c.set_issuables_index_only_actions.include?(c.action_name.to_sym) }
+ before_action :set_issuables_index, if: ->(c) { SET_ISSUEABLES_INDEX_ONLY_ACTIONS.include?(c.action_name.to_sym) }
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb
new file mode 100644
index 00000000000..e610286c1a9
--- /dev/null
+++ b/app/graphql/types/current_user_todos.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Interface to expose todos for the current_user on the `object`
+module Types
+ module CurrentUserTodos
+ include BaseInterface
+
+ field_class Types::BaseField
+
+ field :current_user_todos, Types::TodoType.connection_type,
+ description: 'Todos for the current user',
+ null: false do
+ argument :state, Types::TodoStateEnum,
+ description: 'State of the todos',
+ required: false
+ end
+
+ def current_user_todos(state: nil)
+ state ||= %i(done pending) # TodosFinder treats a `nil` state param as `pending`
+
+ TodosFinder.new(current_user, state: state, type: object.class.name, target_id: object.id).execute
+ end
+ end
+end
diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb
index 3c84dc151bd..4e11a7aaf09 100644
--- a/app/graphql/types/design_management/design_type.rb
+++ b/app/graphql/types/design_management/design_type.rb
@@ -12,6 +12,7 @@ module Types
implements(Types::Notes::NoteableType)
implements(Types::DesignManagement::DesignFields)
+ implements(Types::CurrentUserTodos)
field :versions,
Types::DesignManagement::VersionType.connection_type,
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index df789f3cf47..d6253f74ce5 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
+ implements(Types::CurrentUserTodos)
authorize :read_issue
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 01b02b7976f..805ae111ff7 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -7,6 +7,7 @@ module Types
connection_type_class(Types::CountableConnectionType)
implements(Types::Notes::NoteableType)
+ implements(Types::CurrentUserTodos)
authorize :read_merge_request
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
index 08e7fabeb74..4f21da3d897 100644
--- a/app/graphql/types/todo_type.rb
+++ b/app/graphql/types/todo_type.rb
@@ -26,7 +26,7 @@ module Types
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find }
field :author, Types::UserType,
- description: 'The owner of this todo',
+ description: 'The author of this todo',
null: false,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find }
diff --git a/app/models/concerns/has_wiki.rb b/app/models/concerns/has_wiki.rb
index 3e7cb940a62..df7bbe4dc08 100644
--- a/app/models/concerns/has_wiki.rb
+++ b/app/models/concerns/has_wiki.rb
@@ -25,10 +25,6 @@ module HasWiki
wiki.repository_exists?
end
- def after_wiki_activity
- true
- end
-
private
def check_wiki_path_conflict
diff --git a/app/models/project.rb b/app/models/project.rb
index d588cb791de..4c189197c99 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -3,7 +3,6 @@
require 'carrierwave/orm/activerecord'
class Project < ApplicationRecord
- extend ::Gitlab::Utils::Override
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
include AccessRequestable
@@ -2470,11 +2469,6 @@ class Project < ApplicationRecord
jira_imports.last
end
- override :after_wiki_activity
- def after_wiki_activity
- touch(:last_activity_at, :last_repository_updated_at)
- end
-
def metrics_setting
super || build_metrics_setting
end
diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
index c4fcdff8386..b9916a54d75 100644
--- a/app/models/project_services/chat_message/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -5,6 +5,7 @@ module ChatMessage
attr_reader :merge_request_iid
attr_reader :source_branch
attr_reader :target_branch
+ attr_reader :action
attr_reader :state
attr_reader :title
@@ -16,6 +17,7 @@ module ChatMessage
@merge_request_iid = obj_attr[:iid]
@source_branch = obj_attr[:source_branch]
@target_branch = obj_attr[:target_branch]
+ @action = obj_attr[:action]
@state = obj_attr[:state]
@title = format_title(obj_attr[:title])
end
@@ -63,11 +65,17 @@ module ChatMessage
"#{project_url}/-/merge_requests/#{merge_request_iid}"
end
- # overridden in EE
def state_or_action_text
- state
+ case action
+ when 'approved', 'unapproved'
+ action
+ when 'approval'
+ 'added their approval to'
+ when 'unapproval'
+ 'removed their approval from'
+ else
+ state
+ end
end
end
end
-
-ChatMessage::MergeMessage.prepend_if_ee('::EE::ChatMessage::MergeMessage')
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 5df0a33dc9a..bd570cf7ead 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -10,6 +10,23 @@ class ProjectWiki < Wiki
def disk_path(*args, &block)
container.disk_path + '.wiki'
end
+
+ override :after_wiki_activity
+ def after_wiki_activity
+ # Update activity columns, this is done synchronously to avoid
+ # replication delays in Geo.
+ project.touch(:last_activity_at, :last_repository_updated_at)
+ end
+
+ override :after_post_receive
+ def after_post_receive
+ # Update storage statistics
+ ProjectCacheWorker.perform_async(project.id, [], [:wiki_size])
+
+ # This call is repeated for post-receive, to make sure we're updating
+ # the activity columns for Git pushes as well.
+ after_wiki_activity
+ end
end
# TODO: Remove this once we implement ES support for group wikis.
diff --git a/app/models/wiki.rb b/app/models/wiki.rb
index 22ae9b65564..9462f7401c4 100644
--- a/app/models/wiki.rb
+++ b/app/models/wiki.rb
@@ -133,8 +133,9 @@ class Wiki
commit = commit_details(:created, message, title)
wiki.write_page(title, format.to_sym, content, commit)
+ after_wiki_activity
- update_container_activity
+ true
rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
false
@@ -144,16 +145,18 @@ class Wiki
commit = commit_details(:updated, message, page.title)
wiki.update_page(page.path, title || page.name, format.to_sym, content, commit)
+ after_wiki_activity
- update_container_activity
+ true
end
def delete_page(page, message = nil)
return unless page
wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
+ after_wiki_activity
- update_container_activity
+ true
end
def page_title_and_dir(title)
@@ -209,6 +212,17 @@ class Wiki
web_url(only_path: true).sub(%r{/#{Wiki::HOMEPAGE}\z}, '')
end
+ # Callbacks for synchronous processing after wiki changes.
+ # These will be executed after any change made through GitLab itself (web UI and API),
+ # but not for Git pushes.
+ def after_wiki_activity
+ end
+
+ # Callbacks for background processing after wiki changes.
+ # These will be executed after any change to the wiki repository.
+ def after_post_receive
+ end
+
private
def commit_details(action, message = nil, title = nil)
@@ -225,10 +239,6 @@ class Wiki
def default_message(action, title)
"#{user.username} #{action} page: #{title}"
end
-
- def update_container_activity
- container.after_wiki_activity
- end
end
Wiki.prepend_if_ee('EE::Wiki')
diff --git a/app/services/git/wiki_push_service.rb b/app/services/git/wiki_push_service.rb
index f9de72f2d5f..fa3019ee9d6 100644
--- a/app/services/git/wiki_push_service.rb
+++ b/app/services/git/wiki_push_service.rb
@@ -5,7 +5,16 @@ module Git
# Maximum number of change events we will process on any single push
MAX_CHANGES = 100
+ attr_reader :wiki
+
+ def initialize(wiki, current_user, params)
+ @wiki, @current_user, @params = wiki, current_user, params.dup
+ end
+
def execute
+ # Execute model-specific callbacks
+ wiki.after_post_receive
+
process_changes
end
@@ -23,7 +32,11 @@ module Git
end
def can_process_wiki_events?
- Feature.enabled?(:wiki_events_on_git_push, project)
+ # TODO: Support activity events for group wikis
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/209306
+ return false unless wiki.is_a?(ProjectWiki)
+
+ Feature.enabled?(:wiki_events_on_git_push, wiki.container)
end
def push_changes
@@ -36,10 +49,6 @@ module Git
wiki.repository.raw.raw_changes_between(change[:oldrev], change[:newrev])
end
- def wiki
- project.wiki
- end
-
def create_event_for(change)
event_service.execute(
change.last_known_slug,
@@ -54,7 +63,7 @@ module Git
end
def on_default_branch?(change)
- project.wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
+ wiki.default_branch == ::Gitlab::Git.branch_name(change[:ref])
end
# See: [Gitlab::GitPostReceive#changes]
diff --git a/app/services/git/wiki_push_service/change.rb b/app/services/git/wiki_push_service/change.rb
index 562c43487e9..3d1d0fe8c4e 100644
--- a/app/services/git/wiki_push_service/change.rb
+++ b/app/services/git/wiki_push_service/change.rb
@@ -5,11 +5,11 @@ module Git
class Change
include Gitlab::Utils::StrongMemoize
- # @param [ProjectWiki] wiki
+ # @param [Wiki] wiki
# @param [Hash] change - must have keys `:oldrev` and `:newrev`
# @param [Gitlab::Git::RawDiffChange] raw_change
- def initialize(project_wiki, change, raw_change)
- @wiki, @raw_change, @change = project_wiki, raw_change, change
+ def initialize(wiki, change, raw_change)
+ @wiki, @raw_change, @change = wiki, raw_change, change
end
def page
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index c861f475cac..1c87452f0a3 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -70,6 +70,7 @@
= yield :page_specific_javascripts
= webpack_controller_bundle_tags
+ = webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
= webpack_bundle_tag "chrome_84_icon_fix" if browser.chrome?([">=84", "<84.0.4147.125"]) || browser.edge?([">=84", "<84.0.522.59"])
= yield :project_javascripts
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 4c659241f99..22c2be3b7da 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -14,7 +14,11 @@
%li.divider
- if can?(current_user, :update_user_status, current_user)
%li
- .js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } }
+ %button.btn.menu-item.js-set-status-modal-trigger{ type: 'button' }
+ - if current_user.status.present?
+ = s_('SetStatusModal|Edit status')
+ - else
+ = s_('SetStatusModal|Set status')
- if current_user_menu?(:profile)
%li
= link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username }
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 8f844bd0b47..27dad744d0c 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -12,8 +12,8 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
def perform(gl_repository, identifier, changes, push_options = {})
container, project, repo_type = Gitlab::GlRepository.parse(gl_repository)
- if project.nil? && (!repo_type.snippet? || container.is_a?(ProjectSnippet))
- log("Triggered hook for non-existing project with gl_repository \"#{gl_repository}\"")
+ if container.nil? || (container.is_a?(ProjectSnippet) && project.nil?)
+ log("Triggered hook for non-existing gl_repository \"#{gl_repository}\"")
return false
end
@@ -24,7 +24,7 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
post_received = Gitlab::GitPostReceive.new(container, identifier, changes, push_options)
if repo_type.wiki?
- process_wiki_changes(post_received, container)
+ process_wiki_changes(post_received, container.wiki)
elsif repo_type.project?
process_project_changes(post_received, container)
elsif repo_type.snippet?
@@ -59,18 +59,15 @@ class PostReceive # rubocop:disable Scalability/IdempotentWorker
after_project_changes_hooks(project, user, changes.refs, changes.repository_data)
end
- def process_wiki_changes(post_received, project)
- project.touch(:last_activity_at, :last_repository_updated_at)
- project.wiki.repository.expire_statistics_caches
- ProjectCacheWorker.perform_async(project.id, [], [:wiki_size])
-
+ def process_wiki_changes(post_received, wiki)
user = identify_user(post_received)
return false unless user
# We only need to expire certain caches once per push
- expire_caches(post_received, project.wiki.repository)
+ expire_caches(post_received, wiki.repository)
+ wiki.repository.expire_statistics_caches
- ::Git::WikiPushService.new(project, user, changes: post_received.changes).execute
+ ::Git::WikiPushService.new(wiki, user, changes: post_received.changes).execute
end
def process_snippet_changes(post_received, snippet)