diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-25 21:08:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-25 21:08:15 +0300 |
commit | 28b119a4b47d3a41c4879aab651221b85289bc69 (patch) | |
tree | 5482e008b585e7170a54f7e67e0e62bdb091b7f5 /app/assets/javascripts | |
parent | 4dc41ac252c0bfefb9bc55a8627262cc76c69d5e (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
22 files changed, 184 insertions, 158 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 5119d5021da..60c3f3caf66 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -1,6 +1,5 @@ import $ from 'jquery'; import syntaxHighlight from '~/syntax_highlight'; -import initUserPopovers from '../../user_popovers'; import highlightCurrentUser from './highlight_current_user'; import { renderKroki } from './render_kroki'; import renderMath from './render_math'; @@ -22,7 +21,6 @@ $.fn.renderGFM = function renderGFM() { renderMermaid(this.find('.js-render-mermaid')); } highlightCurrentUser(this.find('.gfm-project_member').get()); - initUserPopovers(this.find('.js-user-link').get()); const issuablePopoverElements = this.find('.gfm-issue, .gfm-merge_request').get(); if (issuablePopoverElements.length) { diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue index dca89133931..8a997624a36 100644 --- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue +++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue @@ -2,29 +2,9 @@ import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; import csrf from '~/lib/utils/csrf'; import { s__ } from '~/locale'; -import SplitButton from '~/vue_shared/components/split_button.vue'; - -const splitButtonActionItems = [ - { - title: s__('ClusterIntegration|Remove integration and resources'), - description: s__( - 'ClusterIntegration|Deletes all GitLab resources attached to this cluster during removal', - ), - eventName: 'remove-cluster-and-cleanup', - }, - { - title: s__('ClusterIntegration|Remove integration'), - description: s__( - 'ClusterIntegration|Removes cluster from project but keeps associated resources', - ), - eventName: 'remove-cluster', - }, -]; export default { - splitButtonActionItems, components: { - SplitButton, GlModal, GlButton, GlFormInput, @@ -79,6 +59,9 @@ export default { canCleanupResources() { return !this.hasManagementProject; }, + buttonCategory() { + return !this.hasManagementProject ? 'secondary' : 'primary'; + }, }, methods: { handleClickRemoveCluster(cleanup = false) { @@ -99,19 +82,20 @@ export default { </script> <template> - <div class="gl-display-flex gl-justify-content-end"> - <split-button + <div class="gl-display-flex"> + <gl-button v-if="canCleanupResources" - :action-items="$options.splitButtonActionItems" - menu-class="dropdown-menu-large" + data-testid="remove-integration-and-resources-button" + class="gl-mr-3" variant="danger" - @remove-cluster="handleClickRemoveCluster(false)" - @remove-cluster-and-cleanup="handleClickRemoveCluster(true)" - /> + @click="handleClickRemoveCluster(true)" + > + {{ s__('ClusterIntegration|Remove integration and resources') }} + </gl-button> <gl-button - v-else + data-testid="remove-integration-button" + :category="buttonCategory" variant="danger" - data-testid="btnRemove" @click="handleClickRemoveCluster(false)" > {{ s__('ClusterIntegration|Remove integration') }} @@ -163,13 +147,7 @@ export default { <template v-if="confirmCleanup"> <gl-button :disabled="!canSubmit" - variant="warning" - category="primary" - @click="handleSubmit" - >{{ s__('ClusterIntegration|Remove integration') }}</gl-button - > - <gl-button - :disabled="!canSubmit" + data-testid="remove-integration-and-resources-modal-button" variant="danger" category="primary" @click="handleSubmit(true)" @@ -179,6 +157,7 @@ export default { <template v-else> <gl-button :disabled="!canSubmit" + data-testid="remove-integration-modal-button" variant="danger" category="primary" @click="handleSubmit" diff --git a/app/assets/javascripts/content_editor/extensions/sourcemap.js b/app/assets/javascripts/content_editor/extensions/sourcemap.js index 94236e2e70e..61d1d983846 100644 --- a/app/assets/javascripts/content_editor/extensions/sourcemap.js +++ b/app/assets/javascripts/content_editor/extensions/sourcemap.js @@ -13,6 +13,7 @@ import Link from './link'; import ListItem from './list_item'; import OrderedList from './ordered_list'; import Paragraph from './paragraph'; +import Strike from './strike'; export default Extension.create({ addGlobalAttributes() { @@ -33,6 +34,7 @@ export default Extension.create({ ListItem.name, OrderedList.name, Paragraph.name, + Strike.name, ], attributes: { sourceMarkdown: { diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index d665f24bba1..cd3ac6fcb1f 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -65,6 +65,7 @@ import { italic, link, code, + strike, } from './serialization_helpers'; const defaultSerializerConfig = { @@ -89,12 +90,7 @@ const defaultSerializerConfig = { close: (...args) => `${defaultMarkdownSerializer.marks.code.close(...args)}$`, escape: false, }, - [Strike.name]: { - open: '~~', - close: '~~', - mixable: true, - expelEnclosingWhitespace: true, - }, + [Strike.name]: strike, ...HTMLMarks.reduce( (acc, { name }) => ({ ...acc, diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js index 899bbcff82f..4cbbfc36151 100644 --- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js +++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js @@ -66,6 +66,10 @@ const factorySpecs = { title: hastNode.properties.title, }), }, + strike: { + type: 'mark', + selector: (hastNode) => ['strike', 's', 'del'].includes(hastNode.tagName), + }, }; export default () => { diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index 089d30edec7..c11ce08de63 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -364,7 +364,7 @@ export function preserveUnchanged(render) { }; } -const generateBoldTags = (open = true) => { +const generateBoldTags = (wrapTagName = openTag) => { return (_, mark) => { const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1]; @@ -375,7 +375,7 @@ const generateBoldTags = (open = true) => { // eslint-disable-next-line @gitlab/require-i18n-strings case '<strong': case '<b': - return (open ? openTag : closeTag)(type.substring(1)); + return wrapTagName(type.substring(1)); default: return '**'; } @@ -384,12 +384,12 @@ const generateBoldTags = (open = true) => { export const bold = { open: generateBoldTags(), - close: generateBoldTags(false), + close: generateBoldTags(closeTag), mixable: true, expelEnclosingWhitespace: true, }; -const generateItalicTag = (open = true) => { +const generateItalicTag = (wrapTagName = openTag) => { return (_, mark) => { const type = /^(\*|_|<em|<i).*/.exec(mark.attrs.sourceMarkdown)?.[1]; @@ -400,7 +400,7 @@ const generateItalicTag = (open = true) => { // eslint-disable-next-line @gitlab/require-i18n-strings case '<em': case '<i': - return (open ? openTag : closeTag)(type.substring(1)); + return wrapTagName(type.substring(1)); default: return '_'; } @@ -409,17 +409,17 @@ const generateItalicTag = (open = true) => { export const italic = { open: generateItalicTag(), - close: generateItalicTag(false), + close: generateItalicTag(closeTag), mixable: true, expelEnclosingWhitespace: true, }; -const generateCodeTag = (open = true) => { +const generateCodeTag = (wrapTagName = openTag) => { return (_, mark) => { const type = /^(`|<code).*/.exec(mark.attrs.sourceMarkdown)?.[1]; if (type === '<code') { - return (open ? openTag : closeTag)(type.substring(1)); + return wrapTagName(type.substring(1)); } return '`'; @@ -428,7 +428,7 @@ const generateCodeTag = (open = true) => { export const code = { open: generateCodeTag(), - close: generateCodeTag(false), + close: generateCodeTag(closeTag), mixable: true, expelEnclosingWhitespace: true, }; @@ -480,3 +480,28 @@ export const link = { return `](${state.esc(canonicalSrc || href)}${title ? ` ${state.quote(title)}` : ''})`; }, }; + +const generateStrikeTag = (wrapTagName = openTag) => { + return (_, mark) => { + const type = /^(~~|<del|<strike|<s).*/.exec(mark.attrs.sourceMarkdown)?.[1]; + + switch (type) { + case '~~': + return type; + /* eslint-disable @gitlab/require-i18n-strings */ + case '<del': + case '<strike': + case '<s': + return wrapTagName(type.substring(1)); + default: + return '~~'; + } + }; +}; + +export const strike = { + open: generateStrikeTag(), + close: generateStrikeTag(closeTag), + mixable: true, + expelEnclosingWhitespace: true, +}; diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue index 3158ae9b126..ccd22085470 100644 --- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue +++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue @@ -22,7 +22,7 @@ export default { type: Boolean, required: true, }, - editProjectServicePath: { + editIntegrationPath: { type: String, required: true, }, @@ -79,7 +79,7 @@ export default { <gl-button variant="success" category="primary" :disabled="!formIsValid" @click="submit"> {{ saveButtonText }} </gl-button> - <gl-button class="float-right" :href="editProjectServicePath">{{ __('Cancel') }}</gl-button> + <gl-button class="float-right" :href="editIntegrationPath">{{ __('Cancel') }}</gl-button> <delete-custom-metric-modal v-if="metricPersisted" :delete-metric-url="customMetricsPath" diff --git a/app/assets/javascripts/custom_metrics/index.js b/app/assets/javascripts/custom_metrics/index.js index 4c279daf5f0..bf572217f5e 100644 --- a/app/assets/javascripts/custom_metrics/index.js +++ b/app/assets/javascripts/custom_metrics/index.js @@ -13,7 +13,7 @@ export default () => { const domEl = document.querySelector(this.$options.el); const { customMetricsPath, - editProjectServicePath, + editIntegrationPath, validateQueryPath, title, query, @@ -30,7 +30,7 @@ export default () => { props: { customMetricsPath, metricPersisted, - editProjectServicePath, + editIntegrationPath, validateQueryPath, formData: { title, diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index 42f4ea8eb58..fc69dca73a7 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -7,8 +7,6 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import initUserPopovers from '../../user_popovers'; - /** * CommitItem * @@ -82,11 +80,6 @@ export default { return this.commit.description_html.replace(/^
/, ''); }, }, - created() { - this.$nextTick(() => { - initUserPopovers(this.$el.querySelectorAll('.js-user-link')); - }); - }, safeHtmlConfig: { ADD_TAGS: ['gl-emoji'], }, diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js index 4e704eb69b2..537a5867096 100644 --- a/app/assets/javascripts/lib/gfm/index.js +++ b/app/assets/javascripts/lib/gfm/index.js @@ -1,10 +1,15 @@ import { unified } from 'unified'; import remarkParse from 'remark-parse'; +import remarkGfm from 'remark-gfm'; import remarkRehype from 'remark-rehype'; import rehypeRaw from 'rehype-raw'; const createParser = () => { - return unified().use(remarkParse).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw); + return unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw); }; const compilerFactory = (renderer) => diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js index bd000bb26fe..670acbbabd7 100644 --- a/app/assets/javascripts/lib/utils/users_cache.js +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -29,8 +29,11 @@ class UsersCache extends Cache { } return getUser(userId).then(({ data }) => { - this.internalStorage[userId] = data; - return data; + this.internalStorage[userId] = { + ...this.get(userId), + ...data, + }; + return this.internalStorage[userId]; }); // missing catch is intentional, error handling depends on use case } diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue index 14d628e455c..4f99e7a2aef 100644 --- a/app/assets/javascripts/members/components/table/members_table.vue +++ b/app/assets/javascripts/members/components/table/members_table.vue @@ -4,7 +4,6 @@ import { mapState } from 'vuex'; import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue'; import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; -import initUserPopovers from '~/user_popovers'; import UserDate from '~/vue_shared/components/user_date.vue'; import { FIELD_KEY_ACTIONS, @@ -85,9 +84,6 @@ export default { return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite; }, }, - mounted() { - initUserPopovers(this.$el.querySelectorAll('.js-user-link')); - }, methods: { hasActionButtons(member) { return ( diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 7d8d23335e0..148a73100ee 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -3,7 +3,6 @@ import { mapGetters, mapActions } from 'vuex'; import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; import createFlash from '~/flash'; import { __ } from '~/locale'; -import initUserPopovers from '~/user_popovers'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import OrderedLayout from '~/vue_shared/components/ordered_layout.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -169,7 +168,6 @@ export default { updated() { this.$nextTick(() => { highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')); - initUserPopovers(this.$el.querySelectorAll('.js-user-link')); }); }, beforeDestroy() { diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/settings/integrations/edit/index.js index 64df0d07d74..64df0d07d74 100644 --- a/app/assets/javascripts/pages/projects/services/edit/index.js +++ b/app/assets/javascripts/pages/projects/settings/integrations/edit/index.js diff --git a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js b/app/assets/javascripts/pages/projects/settings/integrations/index/index.js index 53068f72d3f..53068f72d3f 100644 --- a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/integrations/index/index.js diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue index e35fccf2d7e..05cb2ebb769 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue @@ -36,12 +36,12 @@ export default { }; </script> <template> - <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle gl-my-1"> + <div data-testid="pipeline-mini-graph" class="gl-display-inline gl-vertical-align-middle"> <div v-for="stage in stages" :key="stage.name" :class="stagesClass" - class="stage-container dropdown" + class="dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle stage-container" > <pipeline-stage :stage="stage" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue index 53e21d4ce8b..008b5780ecd 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue @@ -21,6 +21,9 @@ import eventHub from '../../event_hub'; import JobItem from './job_item.vue'; export default { + i18n: { + stage: __('Stage:'), + }, components: { CiIcon, GlLoadingIcon, @@ -48,20 +51,26 @@ export default { }, data() { return { + isDropdownOpen: false, isLoading: false, dropdownContent: [], + stageName: '', }; }, watch: { updateDropdown() { - if (this.updateDropdown && this.isDropdownOpen() && !this.isLoading) { + if (this.updateDropdown && this.isDropdownOpen && !this.isLoading) { this.fetchJobs(); } }, }, methods: { + onHideDropdown() { + this.isDropdownOpen = false; + }, onShowDropdown() { eventHub.$emit('clickedDropdown'); + this.isDropdownOpen = true; this.isLoading = true; this.fetchJobs(); }, @@ -70,6 +79,7 @@ export default { .get(this.stage.dropdown_path) .then(({ data }) => { this.dropdownContent = data.latest_statuses; + this.stageName = data.name; this.isLoading = false; }) .catch(() => { @@ -81,9 +91,6 @@ export default { }); }); }, - isDropdownOpen() { - return this.$el.classList.contains('show'); - }, pipelineActionRequestComplete() { // close the dropdown in MR widget this.$refs.dropdown.hide(); @@ -112,15 +119,17 @@ export default { } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" :toggle-class="['gl-rounded-full!']" menu-class="mini-pipeline-graph-dropdown-menu" + @hide="onHideDropdown" @show="onShowDropdown" > <template #button-content> <ci-icon is-interactive css-classes="gl-rounded-full" + :is-active="isDropdownOpen" :size="24" :status="stage.status" - class="gl-align-items-center gl-display-inline-flex" + class="gl-align-items-center gl-display-inline-flex gl-z-index-1" /> </template> <gl-loading-icon v-if="isLoading" size="sm" /> @@ -129,6 +138,12 @@ export default { class="js-builds-dropdown-list scrollable-menu" data-testid="mini-pipeline-graph-dropdown-menu-list" > + <div + class="gl-align-items-center gl-border-b gl-display-flex gl-font-weight-bold gl-justify-content-center gl-pb-3" + > + <span class="gl-mr-1">{{ $options.i18n.stage }}</span> + <span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span> + </div> <li v-for="job in dropdownContent" :key="job.id"> <job-item :dropdown-length="dropdownContent.length" diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js index 438ae2bc1bc..a3615eab26f 100644 --- a/app/assets/javascripts/user_popovers.js +++ b/app/assets/javascripts/user_popovers.js @@ -1,6 +1,8 @@ import Vue from 'vue'; +import { debounce } from 'lodash'; import UsersCache from './lib/utils/users_cache'; import UserPopover from './vue_shared/components/user_popover/user_popover.vue'; +import { USER_POPOVER_DELAY } from './vue_shared/components/user_popover/constants'; const removeTitle = (el) => { // Removing titles so its not showing tooltips also @@ -59,87 +61,78 @@ const populateUserInfo = (user) => { ); }; -const initializedPopovers = new Map(); -let domObservedForChanges = false; +function createPopover(el, user) { + removeTitle(el); + const preloadedUserInfo = getPreloadedUserInfo(el.dataset); -const addPopoversToModifiedTree = new MutationObserver(() => { - const userLinks = document?.querySelectorAll('.js-user-link, .gfm-project_member'); + Object.assign(user, preloadedUserInfo); - if (userLinks) { - addPopovers(userLinks); /* eslint-disable-line no-use-before-define */ + if (preloadedUserInfo.userId) { + populateUserInfo(user); } -}); + const UserPopoverComponent = Vue.extend(UserPopover); + return new UserPopoverComponent({ + propsData: { + target: el, + user, + show: true, + placement: el.dataset.placement || 'top', + }, + }); +} -function observeBody() { - if (!domObservedForChanges) { - addPopoversToModifiedTree.observe(document.body, { - subtree: true, - childList: true, - }); +function launchPopover(el, mountPopover) { + if (el.user) return; - domObservedForChanges = true; - } + const emptyUser = { + location: null, + bio: null, + workInformation: null, + status: null, + isFollowed: false, + loaded: false, + }; + el.user = emptyUser; + el.addEventListener( + 'mouseleave', + ({ target }) => { + target.removeAttribute('aria-describedby'); + }, + { once: true }, + ); + const popoverInstance = createPopover(el, emptyUser); + + const { userId } = el.dataset; + + popoverInstance.$on('follow', () => { + UsersCache.updateById(userId, { is_followed: true }); + el.user.isFollowed = true; + }); + + popoverInstance.$on('unfollow', () => { + UsersCache.updateById(userId, { is_followed: false }); + el.user.isFollowed = false; + }); + + mountPopover(popoverInstance); } -export default function addPopovers(elements = document.querySelectorAll('.js-user-link')) { - const userLinks = Array.from(elements); - const UserPopoverComponent = Vue.extend(UserPopover); +const userLinkSelector = 'a.js-user-link, a.gfm-project_member'; - observeBody(); +const getUserLinkNode = (node) => node.closest(userLinkSelector); - return userLinks - .filter(({ dataset }) => dataset.user || dataset.userId) - .map((el) => { - if (initializedPopovers.has(el)) { - return initializedPopovers.get(el); - } +const lazyLaunchPopover = debounce((mountPopover, event) => { + const userLink = getUserLinkNode(event.target); + if (userLink) { + launchPopover(userLink, mountPopover); + } +}, USER_POPOVER_DELAY); - const user = { - location: null, - bio: null, - workInformation: null, - status: null, - isFollowed: false, - loaded: false, - }; - const renderedPopover = new UserPopoverComponent({ - propsData: { - target: el, - user, - placement: el.dataset.placement || 'top', - }, - }); - - const { userId } = el.dataset; - - renderedPopover.$on('follow', () => { - UsersCache.updateById(userId, { is_followed: true }); - user.isFollowed = true; - }); - - renderedPopover.$on('unfollow', () => { - UsersCache.updateById(userId, { is_followed: false }); - user.isFollowed = false; - }); - - initializedPopovers.set(el, renderedPopover); - - renderedPopover.$mount(); - - el.addEventListener('mouseenter', ({ target }) => { - removeTitle(target); - const preloadedUserInfo = getPreloadedUserInfo(target.dataset); - - Object.assign(user, preloadedUserInfo); - - if (preloadedUserInfo.userId) { - populateUserInfo(user); - } - }); - el.addEventListener('mouseleave', ({ target }) => { - target.removeAttribute('aria-describedby'); - }); - - return renderedPopover; - }); +let hasAddedLazyPopovers = false; + +export default function addPopovers(mountPopover = (instance) => instance.$mount()) { + if (!hasAddedLazyPopovers) { + document.addEventListener('mouseover', (event) => lazyLaunchPopover(mountPopover, event)); + hasAddedLazyPopovers = true; + } } diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue index c93f620995f..afa5402c28c 100644 --- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue +++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue @@ -18,7 +18,6 @@ import { toggleContainerClasses } from '~/lib/utils/dom_utils'; import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; import Tracking from '~/tracking'; -import initUserPopovers from '~/user_popovers'; import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import MetricImagesTab from '~/vue_shared/components/metric_images/metric_images_tab.vue'; @@ -175,7 +174,6 @@ export default { updated() { this.$nextTick(() => { highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member')); - initUserPopovers(this.$el.querySelectorAll('.js-user-link')); }); }, methods: { diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 9bccc49e894..c70f028a876 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -50,6 +50,11 @@ export default { required: false, default: false, }, + isActive: { + type: Boolean, + required: false, + default: false, + }, isInteractive: { type: Boolean, required: false, @@ -74,8 +79,9 @@ export default { </script> <template> <span - :class="[wrapperStyleClasses, { interactive: isInteractive }]" + :class="[wrapperStyleClasses, { interactive: isInteractive, active: isActive }]" :style="{ height: `${size}px`, width: `${size}px` }" + data-testid="ci-icon-wrapper" > <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/user_popover/constants.js b/app/assets/javascripts/vue_shared/components/user_popover/constants.js new file mode 100644 index 00000000000..1d49aefd297 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_popover/constants.js @@ -0,0 +1 @@ +export const USER_POPOVER_DELAY = 200; diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index ec7a7cd72ae..2eec65457c7 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -14,12 +14,14 @@ import { glEmojiTag } from '~/emoji'; import createFlash from '~/flash'; import { followUser, unfollowUser } from '~/rest_api'; import UserAvatarImage from '../user_avatar/user_avatar_image.vue'; +import { USER_POPOVER_DELAY } from './constants'; const MAX_SKELETON_LINES = 4; export default { name: 'UserPopover', maxSkeletonLines: MAX_SKELETON_LINES, + USER_POPOVER_DELAY, components: { GlIcon, GlLink, @@ -48,6 +50,11 @@ export default { required: false, default: 'top', }, + show: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -133,8 +140,15 @@ export default { </script> <template> - <!-- 200ms delay so not every mouseover triggers Popover --> - <gl-popover :target="target" :delay="200" :placement="placement" boundary="viewport"> + <!-- delay so not every mouseover triggers Popover --> + <gl-popover + :show="show" + :target="target" + :delay="$options.USER_POPOVER_DELAY" + :placement="placement" + boundary="viewport" + triggers="hover focus manual" + > <div class="gl-p-3 gl-line-height-normal gl-display-flex" data-testid="user-popover"> <div class="gl-p-2 flex-shrink-1 gl-display-flex gl-flex-direction-column align-items-center gl-w-70p" |