diff options
author | Sam Bigelow <sbigelow@gitlab.com> | 2019-03-01 16:41:16 -0500 |
---|---|---|
committer | Sam Bigelow <sbigelow@gitlab.com> | 2019-03-21 10:24:18 -0400 |
commit | 1a14e5230e5b7f705fc09c3baf46f6cf74ca3ad0 (patch) | |
tree | 7c8f1d88347252e88535dc087f3129102ef77bf8 /app/assets | |
parent | c174fc0cc1a221bb2ab8f265b3691a17d5cfd8aa (diff) | |
download | gitlab-ce-1a14e5230e5b7f705fc09c3baf46f6cf74ca3ad0.tar.gz |
Add merge request popover with details
- Show pipeline status, title, MR Status and project path
- Popover attached to gitlab flavored markdown everywhere, including:
+ MR/Issue Title
+ MR/Issue description
+ MR/Issue comments
+ Rendered markdown files
Diffstat (limited to 'app/assets')
7 files changed, 206 insertions, 0 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index fc9286d15e6..bfb073fdcdc 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -4,6 +4,7 @@ import renderMath from './render_math'; import renderMermaid from './render_mermaid'; import highlightCurrentUser from './highlight_current_user'; import initUserPopovers from '../../user_popovers'; +import initMRPopovers from '../../mr_popover'; // Render GitLab flavoured Markdown // @@ -15,6 +16,7 @@ $.fn.renderGFM = function renderGFM() { renderMermaid(this.find('.js-render-mermaid')); highlightCurrentUser(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.gfm-project_member').get()); + initMRPopovers(this.find('.gfm-merge_request').get()); return this; }; diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue new file mode 100644 index 00000000000..8e2d8fa816a --- /dev/null +++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue @@ -0,0 +1,110 @@ +<script> +import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; +import Icon from '../../vue_shared/components/icon.vue'; +import CiIcon from '../../vue_shared/components/ci_icon.vue'; +import timeagoMixin from '../../vue_shared/mixins/timeago'; +import query from '../queries/merge_request.graphql'; +import { mrStates, humanMRStates } from '../constants'; + +export default { + name: 'MRPopover', + components: { + GlPopover, + GlSkeletonLoading, + Icon, + CiIcon, + }, + mixins: [timeagoMixin], + props: { + target: { + type: HTMLAnchorElement, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + mergeRequestIID: { + type: String, + required: true, + }, + mergeRequestTitle: { + type: String, + required: true, + }, + }, + data() { + return { + mergeRequest: {}, + }; + }, + computed: { + detailedStatus() { + return this.mergeRequest.headPipeline && this.mergeRequest.headPipeline.detailedStatus; + }, + formattedTime() { + return this.timeFormated(this.mergeRequest.createdAt); + }, + statusBoxClass() { + switch (this.mergeRequest.state) { + case mrStates.merged: + return 'status-box-mr-merged'; + case mrStates.closed: + return 'status-box-closed'; + default: + return 'status-box-open'; + } + }, + stateHumanName() { + switch (this.mergeRequest.state) { + case mrStates.merged: + return humanMRStates.merged; + case mrStates.closed: + return humanMRStates.closed; + default: + return humanMRStates.open; + } + }, + showDetails() { + return Object.keys(this.mergeRequest).length > 0; + }, + }, + apollo: { + mergeRequest: { + query, + update: data => data.project.mergeRequest, + variables() { + const { projectPath, mergeRequestIID } = this; + + return { + projectPath, + mergeRequestIID, + }; + }, + }, + }, +}; +</script> + +<template> + <gl-popover :target="target" boundary="viewport" placement="top" show> + <div class="mr-popover"> + <div v-if="$apollo.loading"> + <gl-skeleton-loading :lines="1" class="animation-container-small mt-1" /> + </div> + <div v-else-if="showDetails" class="d-flex align-items-center justify-content-between"> + <div class="d-inline-flex align-items-center"> + <div :class="`issuable-status-box status-box ${statusBoxClass}`"> + {{ stateHumanName }} + </div> + <span class="text-secondary">Opened <time v-text="formattedTime"></time></span> + </div> + <ci-icon v-if="detailedStatus" :status="detailedStatus" /> + </div> + <h5 class="my-2">{{ mergeRequestTitle }}</h5> + <div class="text-secondary"> + {{ `${projectPath}!${mergeRequestIID}` }} + </div> + </div> + </gl-popover> +</template> diff --git a/app/assets/javascripts/mr_popover/constants.js b/app/assets/javascripts/mr_popover/constants.js new file mode 100644 index 00000000000..433df844c80 --- /dev/null +++ b/app/assets/javascripts/mr_popover/constants.js @@ -0,0 +1,10 @@ +export const mrStates = { + merged: 'merged', + closed: 'closed', +}; + +export const humanMRStates = { + merged: 'Merged', + closed: 'Closed', + open: 'Open', +}; diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js new file mode 100644 index 00000000000..cc686b401d2 --- /dev/null +++ b/app/assets/javascripts/mr_popover/index.js @@ -0,0 +1,62 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import MRPopover from './components/mr_popover.vue'; +import createDefaultClient from '~/lib/graphql'; + +let renderedPopover; +let renderFn; + +const handleUserPopoverMouseOut = ({ target }) => { + target.removeEventListener('mouseleave', handleUserPopoverMouseOut); + + if (renderFn) { + clearTimeout(renderFn); + } + if (renderedPopover) { + renderedPopover.$destroy(); + renderedPopover = null; + } +}; + +/** + * Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes. + * loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover + */ +const handleMRPopoverMount = apolloProvider => ({ target }) => { + // Add listener to actually remove it again + target.addEventListener('mouseleave', handleUserPopoverMouseOut); + + const { projectPath, mrTitle, iid } = target.dataset; + const mergeRequest = {}; + + renderFn = setTimeout(() => { + const MRPopoverComponent = Vue.extend(MRPopover); + renderedPopover = new MRPopoverComponent({ + propsData: { + target, + projectPath, + mergeRequestIID: iid, + mergeRequest, + mergeRequestTitle: mrTitle, + }, + apolloProvider, + }); + + renderedPopover.$mount(); + }, 200); // 200ms delay so not every mouseover triggers Popover + API Call +}; + +export default elements => { + const mrLinks = elements || [...document.querySelectorAll('.gfm-merge_request')]; + if (mrLinks.length > 0) { + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + mrLinks.forEach(el => { + el.addEventListener('mouseenter', handleMRPopoverMount(apolloProvider)); + }); + } +}; diff --git a/app/assets/javascripts/mr_popover/queries/merge_request.graphql b/app/assets/javascripts/mr_popover/queries/merge_request.graphql new file mode 100644 index 00000000000..0bb9bc03bc7 --- /dev/null +++ b/app/assets/javascripts/mr_popover/queries/merge_request.graphql @@ -0,0 +1,14 @@ +query mergeRequest($projectPath: ID!, $mergeRequestIID: ID!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $mergeRequestIID) { + createdAt + state + headPipeline { + detailedStatus { + icon + group + } + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index e6f0a1c69cd..25f80219993 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -22,6 +22,7 @@ import Icon from '../../vue_shared/components/icon.vue'; * - Jobs show view header * - Jobs show view sidebar * - Linked pipelines + * - Extended MR Popover */ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss index 2f4d30fe923..7d46b262a69 100644 --- a/app/assets/stylesheets/components/popover.scss +++ b/app/assets/stylesheets/components/popover.scss @@ -7,3 +7,10 @@ line-height: $gl-line-height; } } + +.mr-popover { + .text-secondary { + font-size: 12px; + line-height: 1.33; + } +} |