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
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/assets/javascripts
parent3ba6a5a16df690246d30bff1038cbed36a16a493 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-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
14 files changed, 389 insertions, 35 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>