diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-11 15:09:28 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-11 15:09:28 +0300 |
commit | 7ca44446277ca15db60b722da28f98f773b8f61a (patch) | |
tree | 7622f430d0431df78dfa34f50efc371458cfb97f /app/assets/javascripts | |
parent | 3ba6a5a16df690246d30bff1038cbed36a16a493 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r-- | app/assets/javascripts/awards_handler.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/commons/vue.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/category.vue | 61 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/emoji_group.vue | 35 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/emoji_list.vue | 44 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/picker.vue | 121 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/components/utils.js | 27 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/constants.js | 14 | ||||
-rw-r--r-- | app/assets/javascripts/emoji/index.js | 16 | ||||
-rw-r--r-- | app/assets/javascripts/notes/components/note_actions.vue | 75 | ||||
-rw-r--r-- | app/assets/javascripts/notes/components/note_form.vue | 2 | ||||
-rw-r--r-- | app/assets/javascripts/notes/components/noteable_note.vue | 1 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/awards_list.vue | 22 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue | 3 |
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> |