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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-03-11 15:09:28 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-03-11 15:09:28 +0300
commit7ca44446277ca15db60b722da28f98f773b8f61a (patch)
tree7622f430d0431df78dfa34f50efc371458cfb97f /app
parent3ba6a5a16df690246d30bff1038cbed36a16a493 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/awards_handler.js1
-rw-r--r--app/assets/javascripts/commons/vue.js2
-rw-r--r--app/assets/javascripts/emoji/components/category.vue61
-rw-r--r--app/assets/javascripts/emoji/components/emoji_group.vue35
-rw-r--r--app/assets/javascripts/emoji/components/emoji_list.vue44
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue121
-rw-r--r--app/assets/javascripts/emoji/components/utils.js27
-rw-r--r--app/assets/javascripts/emoji/constants.js14
-rw-r--r--app/assets/javascripts/emoji/index.js16
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue75
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue3
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/framework/awards.scss2
-rw-r--r--app/assets/stylesheets/framework/emojis.scss23
-rw-r--r--app/assets/stylesheets/page_bundles/members.scss (renamed from app/assets/stylesheets/pages/members.scss)2
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss4
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/finders/repositories/changelog_commits_finder.rb2
-rw-r--r--app/graphql/gitlab_schema.rb2
-rw-r--r--app/graphql/resolvers/base_resolver.rb11
-rw-r--r--app/graphql/resolvers/last_commit_resolver.rb2
-rw-r--r--app/graphql/resolvers/metrics/dashboard_resolver.rb21
-rw-r--r--app/graphql/resolvers/snippets/blobs_resolver.rb1
-rw-r--r--app/graphql/resolvers/tree_resolver.rb2
-rw-r--r--app/graphql/types/base_field.rb27
-rw-r--r--app/graphql/types/current_user_todos.rb5
-rw-r--r--app/graphql/types/snippets/blob_type.rb1
-rw-r--r--app/graphql/types/tree/blob_type.rb1
-rw-r--r--app/mailers/previews/notify_preview.rb4
-rw-r--r--app/services/merge_requests/post_merge_service.rb29
-rw-r--r--app/services/merge_requests/retarget_chain_service.rb34
-rw-r--r--app/views/admin/groups/show.html.haml1
-rw-r--r--app/views/admin/projects/show.html.haml1
-rw-r--r--app/views/groups/group_members/index.html.haml1
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.text.erb3
-rw-r--r--app/views/projects/project_members/index.html.haml1
-rw-r--r--app/workers/merge_requests/delete_source_branch_worker.rb3
40 files changed, 524 insertions, 85 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index b0652a9e2b0..dbdc7e43d2d 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -12,6 +12,7 @@ import axios from './lib/utils/axios_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { __ } from './locale';
+window.axios = axios;
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd';
diff --git a/app/assets/javascripts/commons/vue.js b/app/assets/javascripts/commons/vue.js
index 5b5a1507d38..23647d99656 100644
--- a/app/assets/javascripts/commons/vue.js
+++ b/app/assets/javascripts/commons/vue.js
@@ -6,3 +6,5 @@ if (process.env.NODE_ENV !== 'production') {
}
Vue.use(GlFeatureFlagsPlugin);
+
+Vue.config.ignoredElements = ['gl-emoji'];
diff --git a/app/assets/javascripts/emoji/components/category.vue b/app/assets/javascripts/emoji/components/category.vue
new file mode 100644
index 00000000000..a11122d5403
--- /dev/null
+++ b/app/assets/javascripts/emoji/components/category.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import EmojiGroup from './emoji_group.vue';
+
+export default {
+ components: {
+ GlIntersectionObserver,
+ EmojiGroup,
+ },
+ props: {
+ category: {
+ type: String,
+ required: true,
+ },
+ emojis: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ renderGroup: false,
+ };
+ },
+ computed: {
+ categoryTitle() {
+ return capitalizeFirstCharacter(this.category);
+ },
+ },
+ methods: {
+ categoryAppeared() {
+ this.renderGroup = true;
+ this.$emit('appear', this.category);
+ },
+ categoryDissappeared() {
+ this.renderGroup = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-intersection-observer class="gl-px-5 gl-h-full" @appear="categoryAppeared">
+ <div class="gl-top-0 gl-py-3 gl-w-full emoji-picker-category-header">
+ <b>{{ categoryTitle }}</b>
+ </div>
+ <template v-if="emojis.length">
+ <emoji-group
+ v-for="(emojiGroup, index) in emojis"
+ :key="index"
+ :emojis="emojiGroup"
+ :render-group="renderGroup"
+ :click-emoji="(emoji) => $emit('click', emoji)"
+ />
+ </template>
+ <p v-else>
+ {{ s__('AwardEmoji|No emojis found.') }}
+ </p>
+ </gl-intersection-observer>
+</template>
diff --git a/app/assets/javascripts/emoji/components/emoji_group.vue b/app/assets/javascripts/emoji/components/emoji_group.vue
new file mode 100644
index 00000000000..539cd6963b1
--- /dev/null
+++ b/app/assets/javascripts/emoji/components/emoji_group.vue
@@ -0,0 +1,35 @@
+<script>
+export default {
+ props: {
+ emojis: {
+ type: Array,
+ required: true,
+ },
+ renderGroup: {
+ type: Boolean,
+ required: true,
+ },
+ clickEmoji: {
+ type: Function,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template functional>
+ <div class="gl-display-flex gl-flex-wrap gl-mb-2">
+ <template v-if="props.renderGroup">
+ <button
+ v-for="emoji in props.emojis"
+ :key="emoji"
+ type="button"
+ class="gl-border-0 gl-bg-transparent gl-px-0 gl-py-2 gl-text-center emoji-picker-emoji"
+ data-testid="emoji-button"
+ @click="props.clickEmoji(emoji)"
+ >
+ <gl-emoji :data-name="emoji" />
+ </button>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/emoji/components/emoji_list.vue b/app/assets/javascripts/emoji/components/emoji_list.vue
new file mode 100644
index 00000000000..0d73d751c6d
--- /dev/null
+++ b/app/assets/javascripts/emoji/components/emoji_list.vue
@@ -0,0 +1,44 @@
+<script>
+import { chunk } from 'lodash';
+import { searchEmoji } from '~/emoji';
+import { EMOJIS_PER_ROW } from '../constants';
+import { getEmojiCategories, generateCategoryHeight } from './utils';
+
+export default {
+ props: {
+ searchValue: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return { render: false };
+ },
+ computed: {
+ filteredCategories() {
+ if (this.searchValue !== '') {
+ const emojis = chunk(
+ searchEmoji(this.searchValue).map(({ emoji }) => emoji.name),
+ EMOJIS_PER_ROW,
+ );
+
+ return {
+ search: { emojis, height: generateCategoryHeight(emojis.length) },
+ };
+ }
+
+ return this.categories;
+ },
+ },
+ async mounted() {
+ this.categories = await getEmojiCategories();
+ this.render = true;
+ },
+};
+</script>
+
+<template>
+ <div v-if="render">
+ <slot :filtered-categories="filteredCategories"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
new file mode 100644
index 00000000000..7cd20d82329
--- /dev/null
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -0,0 +1,121 @@
+<script>
+import { GlIcon, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import VirtualList from 'vue-virtual-scroll-list';
+import { CATEGORY_NAMES } from '~/emoji';
+import { CATEGORY_ICON_MAP } from '../constants';
+import Category from './category.vue';
+import EmojiList from './emoji_list.vue';
+import { getEmojiCategories } from './utils';
+
+export default {
+ components: {
+ GlIcon,
+ GlDropdown,
+ GlSearchBoxByType,
+ VirtualList,
+ Category,
+ EmojiList,
+ },
+ props: {
+ toggleClass: {
+ type: [Array, String, Object],
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ currentCategory: null,
+ searchValue: '',
+ };
+ },
+ computed: {
+ categoryNames() {
+ return CATEGORY_NAMES.map((category) => ({
+ name: category,
+ icon: CATEGORY_ICON_MAP[category],
+ }));
+ },
+ },
+ methods: {
+ categoryAppeared(category) {
+ this.currentCategory = category;
+ },
+ async scrollToCategory(categoryName) {
+ const categories = await getEmojiCategories();
+ const { top } = categories[categoryName];
+
+ this.$refs.virtualScoller.setScrollTop(top);
+ },
+ selectEmoji(name) {
+ this.$emit('click', name);
+ this.$refs.dropdown.hide();
+ },
+ getBoundaryElement() {
+ return document.querySelector('.content-wrapper') || 'scrollParent';
+ },
+ onSearchInput() {
+ this.$refs.virtualScoller.setScrollTop(0);
+ this.$refs.virtualScoller.forceRender();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="emoji-picker">
+ <gl-dropdown
+ ref="dropdown"
+ :toggle-class="toggleClass"
+ :boundary="getBoundaryElement()"
+ menu-class="dropdown-extended-height"
+ no-flip
+ right
+ lazy
+ >
+ <template #button-content><slot name="button-content"></slot></template>
+ <gl-search-box-by-type
+ v-model="searchValue"
+ class="gl-mx-5! gl-mb-2!"
+ autofocus
+ debounce="500"
+ @input="onSearchInput"
+ />
+ <div
+ v-show="!searchValue"
+ class="gl-display-flex gl-mx-5 gl-border-b-solid gl-border-gray-100 gl-border-b-1"
+ >
+ <button
+ v-for="category in categoryNames"
+ :key="category.name"
+ :class="{
+ 'gl-text-black-normal! emoji-picker-category-active': category.name === currentCategory,
+ }"
+ type="button"
+ class="gl-border-0 gl-border-b-2 gl-border-b-solid gl-flex-fill-1 gl-text-gray-300 gl-pt-3 gl-pb-3 gl-bg-transparent emoji-picker-category-tab"
+ @click="scrollToCategory(category.name)"
+ >
+ <gl-icon :name="category.icon" :size="12" />
+ </button>
+ </div>
+ <emoji-list :search-value="searchValue">
+ <template #default="{ filteredCategories }">
+ <virtual-list ref="virtualScoller" :size="258" :remain="1" :bench="2" variable>
+ <div
+ v-for="(category, categoryKey) in filteredCategories"
+ :key="categoryKey"
+ :style="{ height: category.height + 'px' }"
+ >
+ <category
+ :category="categoryKey"
+ :emojis="category.emojis"
+ @appear="categoryAppeared"
+ @click="selectEmoji"
+ />
+ </div>
+ </virtual-list>
+ </template>
+ </emoji-list>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/emoji/components/utils.js b/app/assets/javascripts/emoji/components/utils.js
new file mode 100644
index 00000000000..b95b56a1d6f
--- /dev/null
+++ b/app/assets/javascripts/emoji/components/utils.js
@@ -0,0 +1,27 @@
+import { chunk, memoize } from 'lodash';
+import { initEmojiMap, getEmojiCategoryMap } from '~/emoji';
+import { EMOJIS_PER_ROW, EMOJI_ROW_HEIGHT, CATEGORY_ROW_HEIGHT } from '../constants';
+
+export const generateCategoryHeight = (emojisLength) =>
+ emojisLength * EMOJI_ROW_HEIGHT + CATEGORY_ROW_HEIGHT;
+
+export const getEmojiCategories = memoize(async () => {
+ await initEmojiMap();
+
+ const categories = await getEmojiCategoryMap();
+ let top = 0;
+
+ return Object.freeze(
+ Object.keys(categories).reduce((acc, category) => {
+ const emojis = chunk(categories[category], EMOJIS_PER_ROW);
+ const height = generateCategoryHeight(emojis.length);
+ const newAcc = {
+ ...acc,
+ [category]: { emojis, height, top },
+ };
+ top += height;
+
+ return newAcc;
+ }, {}),
+ );
+});
diff --git a/app/assets/javascripts/emoji/constants.js b/app/assets/javascripts/emoji/constants.js
new file mode 100644
index 00000000000..bf73d1ca5a9
--- /dev/null
+++ b/app/assets/javascripts/emoji/constants.js
@@ -0,0 +1,14 @@
+export const CATEGORY_ICON_MAP = {
+ activity: 'dumbbell',
+ people: 'smiley',
+ nature: 'nature',
+ food: 'food',
+ travel: 'car',
+ objects: 'object',
+ symbols: 'heart',
+ flags: 'flag',
+};
+
+export const EMOJIS_PER_ROW = 9;
+export const EMOJI_ROW_HEIGHT = 34;
+export const CATEGORY_ROW_HEIGHT = 37;
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index d022fcbeabe..d3b658a4020 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -2,6 +2,7 @@ import { escape, minBy } from 'lodash';
import emojiAliases from 'emojis/aliases.json';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
+import { CATEGORY_ICON_MAP } from './constants';
let emojiMap = null;
let validEmojiNames = null;
@@ -155,19 +156,14 @@ export function sortEmoji(items) {
return [...items].sort((a, b) => a.score - b.score || a.fieldValue.localeCompare(b.fieldValue));
}
+export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
+
let emojiCategoryMap;
export function getEmojiCategoryMap() {
if (!emojiCategoryMap) {
- emojiCategoryMap = {
- activity: [],
- people: [],
- nature: [],
- food: [],
- travel: [],
- objects: [],
- symbols: [],
- flags: [],
- };
+ emojiCategoryMap = CATEGORY_NAMES.reduce((acc, category) => {
+ return { ...acc, [category]: [] };
+ }, {});
Object.keys(emojiMap).forEach((name) => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.c]) {
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 0b2c7611f8e..ed6701b34e8 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import Api from '~/api';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
import { deprecatedCreateFlash as flash } from '~/flash';
@@ -8,6 +8,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { splitCamelCase } from '../../lib/utils/text_utility';
import ReplyButton from './note_actions/reply_button.vue';
@@ -19,11 +20,12 @@ export default {
GlButton,
GlDropdownItem,
UserAccessRoleBadge,
+ EmojiPicker: () => import('~/emoji/components/picker.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [resolvedStatusMixin],
+ mixins: [resolvedStatusMixin, glFeatureFlagsMixin()],
props: {
author: {
type: Object,
@@ -117,6 +119,10 @@ export default {
type: Boolean,
required: true,
},
+ awardPath: {
+ type: String,
+ required: true,
+ },
},
computed: {
...mapGetters(['getUserDataByProp', 'getNoteableData']),
@@ -185,6 +191,7 @@ export default {
},
},
methods: {
+ ...mapActions(['toggleAwardRequest']),
onEdit() {
this.$emit('handleEdit');
},
@@ -222,6 +229,13 @@ export default {
.catch(() => flash(__('Something went wrong while updating assignees')));
}
},
+ setAwardEmoji(awardName) {
+ this.toggleAwardRequest({
+ endpoint: this.awardPath,
+ noteId: this.noteId,
+ awardName,
+ });
+ },
},
};
</script>
@@ -267,28 +281,41 @@ export default {
class="line-resolve-btn note-action-button"
@click="onResolve"
/>
- <gl-button
- v-if="canAwardEmoji"
- v-gl-tooltip
- :class="{ 'js-user-authored': isAuthoredByCurrentUser }"
- class="note-action-button note-emoji-button add-reaction-button js-add-award js-note-emoji"
- category="tertiary"
- variant="default"
- size="small"
- title="Add reaction"
- data-position="right"
- :aria-label="__('Add reaction')"
- >
- <span class="reaction-control-icon reaction-control-icon-neutral">
- <gl-icon name="slight-smile" />
- </span>
- <span class="reaction-control-icon reaction-control-icon-positive">
- <gl-icon name="smiley" />
- </span>
- <span class="reaction-control-icon reaction-control-icon-super-positive">
- <gl-icon name="smile" />
- </span>
- </gl-button>
+ <template v-if="canAwardEmoji">
+ <emoji-picker
+ v-if="glFeatures.improvedEmojiPicker"
+ toggle-class="note-action-button note-emoji-button gl-text-gray-600 gl-m-2 gl-p-0! gl-shadow-none! gl-bg-transparent!"
+ @click="setAwardEmoji"
+ >
+ <template #button-content>
+ <gl-icon class="link-highlight award-control-icon-neutral gl-m-0!" name="slight-smile" />
+ <gl-icon class="link-highlight award-control-icon-positive gl-m-0!" name="smiley" />
+ <gl-icon class="link-highlight award-control-icon-super-positive gl-m-0!" name="smile" />
+ </template>
+ </emoji-picker>
+ <gl-button
+ v-else
+ v-gl-tooltip
+ :class="{ 'js-user-authored': isAuthoredByCurrentUser }"
+ class="note-action-button note-emoji-button add-reaction-button js-add-award js-note-emoji"
+ category="tertiary"
+ variant="default"
+ size="small"
+ title="Add reaction"
+ data-position="right"
+ :aria-label="__('Add reaction')"
+ >
+ <span class="reaction-control-icon reaction-control-icon-neutral">
+ <gl-icon name="slight-smile" />
+ </span>
+ <span class="reaction-control-icon reaction-control-icon-positive">
+ <gl-icon name="smiley" />
+ </span>
+ <span class="reaction-control-icon reaction-control-icon-super-positive">
+ <gl-icon name="smile" />
+ </span>
+ </gl-button>
+ </template>
<reply-button
v-if="showReply"
ref="replyButton"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index a28c467117a..d74ade15de1 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -201,7 +201,7 @@ export default {
changedCommentText() {
return sprintf(
__(
- 'This comment has changed since you started editing, please review the %{startTag}updated comment%{endTag} to ensure information is not lost.',
+ 'This comment changed after you started editing it. Review the %{startTag}updated comment%{endTag} to ensure information is not lost.',
),
{
startTag: `<a href="${this.noteHash}" target="_blank" rel="noopener noreferrer">`,
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 4343fac3cfa..9bf1496f479 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -416,6 +416,7 @@ export default {
:is-draft="note.isDraft"
:resolve-discussion="note.isDraft && note.resolve_discussion"
:discussion-id="discussionId"
+ :award-path="note.toggle_award_path"
@handleEdit="editHandler"
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index ce67d33d4a1..82b3545117f 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -2,7 +2,9 @@
/* eslint-disable vue/no-v-html */
import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { groupBy } from 'lodash';
+import EmojiPicker from '~/emoji/components/picker.vue';
import { __, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { glEmojiTag } from '../../emoji';
// Internal constant, specific to this component, used when no `currentUserId` is given
@@ -12,10 +14,12 @@ export default {
components: {
GlButton,
GlIcon,
+ EmojiPicker,
},
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
awards: {
type: Array,
@@ -166,7 +170,25 @@ export default {
<span class="js-counter">{{ awardList.list.length }}</span>
</gl-button>
<div v-if="canAwardEmoji" class="award-menu-holder">
+ <emoji-picker
+ v-if="glFeatures.improvedEmojiPicker"
+ toggle-class="add-reaction-button gl-relative!"
+ @click="handleAward"
+ >
+ <template #button-content>
+ <span class="reaction-control-icon reaction-control-icon-neutral">
+ <gl-icon name="slight-smile" />
+ </span>
+ <span class="reaction-control-icon reaction-control-icon-positive">
+ <gl-icon name="smiley" />
+ </span>
+ <span class="reaction-control-icon reaction-control-icon-super-positive">
+ <gl-icon name="smile" />
+ </span>
+ </template>
+ </emoji-picker>
<gl-button
+ v-else
v-gl-tooltip.viewport
:class="addButtonClass"
class="add-reaction-button js-add-award"
diff --git a/app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue b/app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue
new file mode 100644
index 00000000000..35f9ac14681
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue
@@ -0,0 +1,3 @@
+<template>
+ <div class="timeline-icon"><slot></slot></div>
+</template>
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index e55654e84d8..4a15e0eb458 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -14,7 +14,6 @@
@import './pages/issues';
@import './pages/labels';
@import './pages/login';
-@import './pages/members';
@import './pages/merge_requests';
@import './pages/monitor';
@import './pages/note_form';
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index a7623b65539..662f7f52d61 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -274,7 +274,9 @@
// `position:absolute`
&::after {
content: '\a0';
+ display: block !important;
width: 1em;
+ color: transparent;
}
.reaction-control-icon {
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 13c5541da92..c5c660c1014 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -16,3 +16,26 @@ gl-emoji {
vertical-align: baseline;
}
}
+
+.emoji-picker-category-header {
+ @include gl-sticky;
+ background-color: $white-transparent;
+}
+
+.emoji-picker-emoji {
+ height: 30px;
+ // Create a width that fits 9 emojis per row
+ width: 100 / 9 * 1%;
+}
+
+.emoji-picker .gl-new-dropdown .dropdown-menu {
+ width: 350px;
+}
+
+.emoji-picker-category-tab {
+ border-bottom-color: transparent;
+}
+
+.emoji-picker .gl-new-dropdown-inner > :last-child {
+ padding-bottom: 0;
+}
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/page_bundles/members.scss
index 0ccde57746a..7b4c74b8253 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/page_bundles/members.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.project-members-title {
padding-bottom: 10px;
border-bottom: 1px solid $border-color;
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index aae9f3ded4f..5b3e2ab4cd0 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -205,6 +205,10 @@
}
}
+ .emoji-picker-category-active {
+ border-bottom-color: $active-tab-border;
+ }
+
.branch-header-title {
color: $border-and-box-shadow;
}
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 17138da6e2b..c454ae6eaf4 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -44,6 +44,7 @@ class Projects::IssuesController < Projects::ApplicationController
push_frontend_feature_flag(:tribute_autocomplete, @project)
push_frontend_feature_flag(:vue_issuables_list, project)
push_frontend_feature_flag(:usage_data_design_action, project, default_enabled: true)
+ push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
end
before_action only: :show do
diff --git a/app/finders/repositories/changelog_commits_finder.rb b/app/finders/repositories/changelog_commits_finder.rb
index 08f1144701a..b80b8e94e59 100644
--- a/app/finders/repositories/changelog_commits_finder.rb
+++ b/app/finders/repositories/changelog_commits_finder.rb
@@ -93,7 +93,7 @@ module Repositories
end
def revert_commit_sha(commit)
- matches = commit.description.match(REVERT_REGEX)
+ matches = commit.description&.match(REVERT_REGEX)
matches[:sha] if matches
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index d66a2333d11..7ab5dc36e4a 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -13,8 +13,6 @@ class GitlabSchema < GraphQL::Schema
use GraphQL::Pagination::Connections
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
- use Gitlab::Graphql::Present
- use Gitlab::Graphql::CallsGitaly
use Gitlab::Graphql::Pagination::Connections
use Gitlab::Graphql::GenericTracing
use Gitlab::Graphql::Timeout, max_seconds: Gitlab.config.gitlab.graphql_timeout
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 5db618254cb..67bba079512 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -12,8 +12,17 @@ module Resolvers
@requires_argument = true
end
+ def self.calls_gitaly!
+ @calls_gitaly = true
+ end
+
def self.field_options
- super.merge(requires_argument: @requires_argument)
+ extra_options = {
+ requires_argument: @requires_argument,
+ calls_gitaly: @calls_gitaly
+ }.compact
+
+ super.merge(extra_options)
end
def self.singular_type
diff --git a/app/graphql/resolvers/last_commit_resolver.rb b/app/graphql/resolvers/last_commit_resolver.rb
index dd89c322617..00c43bdfee6 100644
--- a/app/graphql/resolvers/last_commit_resolver.rb
+++ b/app/graphql/resolvers/last_commit_resolver.rb
@@ -4,6 +4,8 @@ module Resolvers
class LastCommitResolver < BaseResolver
type Types::CommitType, null: true
+ calls_gitaly!
+
alias_method :tree, :object
def resolve(**args)
diff --git a/app/graphql/resolvers/metrics/dashboard_resolver.rb b/app/graphql/resolvers/metrics/dashboard_resolver.rb
index f569cb0b2c3..a82a4a95254 100644
--- a/app/graphql/resolvers/metrics/dashboard_resolver.rb
+++ b/app/graphql/resolvers/metrics/dashboard_resolver.rb
@@ -3,19 +3,30 @@
module Resolvers
module Metrics
class DashboardResolver < Resolvers::BaseResolver
+ type Types::Metrics::DashboardType, null: true
+ calls_gitaly!
+
argument :path, GraphQL::STRING_TYPE,
required: true,
- description: "Path to a file which defines metrics dashboard eg: 'config/prometheus/common_metrics.yml'."
-
- type Types::Metrics::DashboardType, null: true
+ description: "Path to a file which defines metrics dashboard " \
+ "eg: 'config/prometheus/common_metrics.yml'."
alias_method :environment, :object
def resolve(**args)
return unless environment
- ::PerformanceMonitoring::PrometheusDashboard
- .find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
+ ::PerformanceMonitoring::PrometheusDashboard.find_for(**args, **service_params)
+ end
+
+ private
+
+ def service_params
+ {
+ project: environment.project,
+ user: current_user,
+ options: { environment: environment }
+ }
end
end
end
diff --git a/app/graphql/resolvers/snippets/blobs_resolver.rb b/app/graphql/resolvers/snippets/blobs_resolver.rb
index 672214df7d5..569b82149d3 100644
--- a/app/graphql/resolvers/snippets/blobs_resolver.rb
+++ b/app/graphql/resolvers/snippets/blobs_resolver.rb
@@ -8,6 +8,7 @@ module Resolvers
type Types::Snippets::BlobType.connection_type, null: true
authorize :read_snippet
+ calls_gitaly!
alias_method :snippet, :object
diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb
index 7a70c35897d..c07d9187d4d 100644
--- a/app/graphql/resolvers/tree_resolver.rb
+++ b/app/graphql/resolvers/tree_resolver.rb
@@ -4,6 +4,8 @@ module Resolvers
class TreeResolver < BaseResolver
type Types::Tree::TreeType, null: true
+ calls_gitaly!
+
argument :path, GraphQL::STRING_TYPE,
required: false,
default_value: '',
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index f85675f72f2..78ab6890923 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -9,16 +9,25 @@ module Types
DEFAULT_COMPLEXITY = 1
- def initialize(*args, **kwargs, &block)
+ def initialize(**kwargs, &block)
@calls_gitaly = !!kwargs.delete(:calls_gitaly)
- @constant_complexity = !!kwargs[:complexity]
+ @constant_complexity = kwargs[:complexity].is_a?(Integer) && kwargs[:complexity] > 0
@requires_argument = !!kwargs.delete(:requires_argument)
kwargs[:complexity] = field_complexity(kwargs[:resolver_class], kwargs[:complexity])
@feature_flag = kwargs[:feature_flag]
kwargs = check_feature_flag(kwargs)
kwargs = gitlab_deprecation(kwargs)
- super(*args, **kwargs, &block)
+ super(**kwargs, &block)
+
+ # We want to avoid the overhead of this in prod
+ extension ::Gitlab::Graphql::CallsGitaly::FieldExtension if Gitlab.dev_or_test_env?
+
+ extension ::Gitlab::Graphql::Present::FieldExtension
+ end
+
+ def may_call_gitaly?
+ @constant_complexity || @calls_gitaly
end
def requires_argument?
@@ -54,8 +63,10 @@ module Types
end
def check_feature_flag(args)
- args[:description] = feature_documentation_message(args[:feature_flag], args[:description]) if args[:feature_flag].present?
- args.delete(:feature_flag)
+ ff = args.delete(:feature_flag)
+ return args unless ff.present?
+
+ args[:description] = feature_documentation_message(ff, args[:description])
args
end
@@ -78,7 +89,9 @@ module Types
# items which can be loaded.
proc do |ctx, args, child_complexity|
# Resolvers may add extra complexity depending on used arguments
- complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i
+ complexity = child_complexity + resolver&.try(
+ :resolver_complexity, args, child_complexity: child_complexity
+ ).to_i
complexity += 1 if calls_gitaly?
complexity += complexity * connection_complexity_multiplier(ctx, args)
@@ -93,7 +106,7 @@ module Types
page_size = field_defn.connection_max_page_size || ctx.schema.default_max_page_size
limit_value = [args[:first], args[:last], page_size].compact.min
- multiplier = self.resolver&.try(:complexity_multiplier, args).to_f
+ multiplier = resolver&.try(:complexity_multiplier, args).to_f
limit_value * multiplier
end
end
diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb
index 79a430af1d7..2551db875b0 100644
--- a/app/graphql/types/current_user_todos.rb
+++ b/app/graphql/types/current_user_todos.rb
@@ -16,9 +16,10 @@ module Types
end
def current_user_todos(state: nil)
- state ||= %i(done pending) # TodosFinder treats a `nil` state param as `pending`
+ state ||= %i[done pending] # TodosFinder treats a `nil` state param as `pending`
+ klass = unpresented.class
- TodosFinder.new(current_user, state: state, type: object.class.name, target_id: object.id).execute
+ TodosFinder.new(current_user, state: state, type: klass.name, target_id: object.id).execute
end
end
end
diff --git a/app/graphql/types/snippets/blob_type.rb b/app/graphql/types/snippets/blob_type.rb
index fb0c1d9409b..fb9ee380705 100644
--- a/app/graphql/types/snippets/blob_type.rb
+++ b/app/graphql/types/snippets/blob_type.rb
@@ -14,7 +14,6 @@ module Types
field :plain_data, GraphQL::STRING_TYPE,
description: 'Blob plain highlighted data.',
- calls_gitaly: true,
null: true
field :raw_path, GraphQL::STRING_TYPE,
diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb
index 3823bd94083..d192c8d3c57 100644
--- a/app/graphql/types/tree/blob_type.rb
+++ b/app/graphql/types/tree/blob_type.rb
@@ -15,6 +15,7 @@ module Types
field :web_path, GraphQL::STRING_TYPE, null: true,
description: 'Web path of the blob.'
field :lfs_oid, GraphQL::STRING_TYPE, null: true,
+ calls_gitaly: true,
description: 'LFS ID of the blob.'
field :mode, GraphQL::STRING_TYPE, null: true,
description: 'Blob mode in numeric format.'
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index dee91789f50..36c6f2f6c79 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -60,6 +60,10 @@ class NotifyPreview < ActionMailer::Preview
end
end
+ def new_mention_in_merge_request_email
+ Notify.new_mention_in_merge_request_email(user.id, issue.id, user.id).message
+ end
+
def closed_issue_email
Notify.closed_issue_email(user.id, issue.id, user.id).message
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index aafba9bfcef..4d7d632ee14 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -20,7 +20,6 @@ module MergeRequests
merge_request_activity_counter.track_merge_mr_action(user: current_user)
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
- retarget_chain_merge_requests(merge_request)
invalidate_cache_counts(merge_request, users: merge_request.assignees | merge_request.reviewers)
merge_request.update_project_counter_caches
delete_non_latest_diffs(merge_request)
@@ -31,34 +30,6 @@ module MergeRequests
private
- def retarget_chain_merge_requests(merge_request)
- return unless Feature.enabled?(:retarget_merge_requests, merge_request.target_project)
-
- # we can only retarget MRs that are targeting the same project
- # and have a remove source branch set
- return unless merge_request.for_same_project? && merge_request.remove_source_branch?
-
- # find another merge requests that
- # - as a target have a current source project and branch
- other_merge_requests = merge_request.source_project
- .merge_requests
- .opened
- .by_target_branch(merge_request.source_branch)
- .preload_source_project
- .at_most(MAX_RETARGET_MERGE_REQUESTS)
-
- other_merge_requests.find_each do |other_merge_request|
- # Update only MRs on projects that we have access to
- next unless can?(current_user, :update_merge_request, other_merge_request.source_project)
-
- ::MergeRequests::UpdateService
- .new(other_merge_request.source_project, current_user,
- target_branch: merge_request.target_branch,
- target_branch_was_deleted: true)
- .execute(other_merge_request)
- end
- end
-
def close_issues(merge_request)
return unless merge_request.target_branch == project.default_branch
diff --git a/app/services/merge_requests/retarget_chain_service.rb b/app/services/merge_requests/retarget_chain_service.rb
new file mode 100644
index 00000000000..f24d67243c9
--- /dev/null
+++ b/app/services/merge_requests/retarget_chain_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ class RetargetChainService < MergeRequests::BaseService
+ MAX_RETARGET_MERGE_REQUESTS = 4
+
+ def execute(merge_request)
+ return unless Feature.enabled?(:retarget_merge_requests, merge_request.target_project, default_enabled: :yaml)
+
+ # we can only retarget MRs that are targeting the same project
+ return unless merge_request.for_same_project? && merge_request.merged?
+
+ # find another merge requests that
+ # - as a target have a current source project and branch
+ other_merge_requests = merge_request.source_project
+ .merge_requests
+ .opened
+ .by_target_branch(merge_request.source_branch)
+ .preload_source_project
+ .at_most(MAX_RETARGET_MERGE_REQUESTS)
+
+ other_merge_requests.find_each do |other_merge_request|
+ # Update only MRs on projects that we have access to
+ next unless can?(current_user, :update_merge_request, other_merge_request.source_project)
+
+ ::MergeRequests::UpdateService
+ .new(other_merge_request.source_project, current_user,
+ target_branch: merge_request.target_branch,
+ target_branch_was_deleted: true)
+ .execute(other_merge_request)
+ end
+ end
+ end
+end
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index d8128d2fa7e..f8c490dd948 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/members'
- add_to_breadcrumbs _("Groups"), admin_groups_path
- breadcrumb_title @group.name
- page_title @group.name, _("Groups")
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 2085515e349..40443fb3406 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/members'
- add_to_breadcrumbs _("Projects"), admin_projects_path
- breadcrumb_title @project.full_name
- page_title @project.full_name, _("Projects")
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index df39e6297f6..da00879ecf9 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/members'
- page_title _('Group members')
- can_manage_members = can?(current_user, :admin_group_member, @group)
- show_invited_members = can_manage_members && @invited_members.exists?
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
index 3c78e257a88..0121006852c 100644
--- a/app/views/notify/new_mention_in_merge_request_email.text.erb
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -4,6 +4,7 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= sanitize_name(@merge_request.author_name) %>
-= assignees_label(@merge_request)
+<%= assignees_label(@merge_request) %>
+<%= reviewers_label(@merge_request) %>
<%= @merge_request.description %>
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 6bc9bcf6b90..c88dae079ae 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,3 +1,4 @@
+- add_page_specific_style 'page_bundles/members'
- page_title _("Members")
.js-remove-member-modal
diff --git a/app/workers/merge_requests/delete_source_branch_worker.rb b/app/workers/merge_requests/delete_source_branch_worker.rb
index 99816d5ed0b..eb83d10af33 100644
--- a/app/workers/merge_requests/delete_source_branch_worker.rb
+++ b/app/workers/merge_requests/delete_source_branch_worker.rb
@@ -16,6 +16,9 @@ class MergeRequests::DeleteSourceBranchWorker
::Branches::DeleteService.new(merge_request.source_project, user)
.execute(merge_request.source_branch)
+
+ ::MergeRequests::RetargetChainService.new(merge_request.source_project, user)
+ .execute(merge_request)
rescue ActiveRecord::RecordNotFound
end
end