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>2022-05-25 21:08:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-25 21:08:15 +0300
commit28b119a4b47d3a41c4879aab651221b85289bc69 (patch)
tree5482e008b585e7170a54f7e67e0e62bdb091b7f5 /app/assets/javascripts
parent4dc41ac252c0bfefb9bc55a8627262cc76c69d5e (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js2
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue51
-rw-r--r--app/assets/javascripts/content_editor/extensions/sourcemap.js2
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js8
-rw-r--r--app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js4
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js43
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form.vue4
-rw-r--r--app/assets/javascripts/custom_metrics/index.js4
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue7
-rw-r--r--app/assets/javascripts/lib/gfm/index.js7
-rw-r--r--app/assets/javascripts/lib/utils/users_cache.js7
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue4
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue2
-rw-r--r--app/assets/javascripts/pages/projects/settings/integrations/edit/index.js (renamed from app/assets/javascripts/pages/projects/services/edit/index.js)0
-rw-r--r--app/assets/javascripts/pages/projects/settings/integrations/index/index.js (renamed from app/assets/javascripts/pages/projects/settings/integrations/show/index.js)0
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_mini_graph.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue25
-rw-r--r--app/assets/javascripts/user_popovers.js139
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue18
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(/^&#x000A;/, '');
},
},
- 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"