Skip to content
Snippets Groups Projects
Commit 5cd8abbf authored by Coung Ngo's avatar Coung Ngo
Browse files

Merge branch '415652-add-epic-popover-in-gfm' into 'master'

Show Popover on Epic links within Markdown

See merge request !124055



Merged-by: default avatarCoung Ngo <cngo@gitlab.com>
Approved-by: default avatarArtur Fedorov <afedorov@gitlab.com>
Approved-by: default avatarBrett Walker <bwalker@gitlab.com>
Approved-by: Ali Ndlovu's avatarAli Ndlovu <andlovu@gitlab.com>
Approved-by: default avatarCoung Ngo <cngo@gitlab.com>
Reviewed-by: Kushal Pandya's avatarKushal Pandya <kushal@gitlab.com>
Reviewed-by: default avatarCoung Ngo <cngo@gitlab.com>
Reviewed-by: default avatarArtur Fedorov <afedorov@gitlab.com>
Co-authored-by: Kushal Pandya's avatarKushal Pandya <kushal@gitlab.com>
parents b84faeef daa34e71
No related branches found
No related tags found
1 merge request!124055Show Popover on Epic links within Markdown
Pipeline #911991967 passed
Showing
with 499 additions and 108 deletions
......@@ -9,7 +9,7 @@ import { renderJSONTable } from './render_json_table';
function initPopovers(elements) {
if (!elements.length) return;
import(/* webpackChunkName: 'IssuablePopoverBundle' */ '~/issuable/popover')
import(/* webpackChunkName: 'IssuablePopoverBundle' */ 'ee_else_ce/issuable/popover')
.then(({ default: initIssuablePopovers }) => {
initIssuablePopovers(elements);
})
......@@ -39,7 +39,7 @@ export function renderGFM(element) {
'.js-render-mermaid',
'[lang="json"][data-lang-params="table"]',
'.gfm-project_member',
'.gfm-issue, .gfm-work_item, .gfm-merge_request',
'.gfm-issue, .gfm-work_item, .gfm-merge_request, .gfm-epic',
'.js-render-metrics',
'.js-render-observability',
].map((selector) => Array.from(element.querySelectorAll(selector)));
......
......@@ -4,7 +4,13 @@ import Vue from 'vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
import { __ } from '~/locale';
import { STATUS_CLOSED, STATUS_OPEN, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import {
STATUS_CLOSED,
STATUS_OPEN,
TYPE_ISSUE,
TYPE_MERGE_REQUEST,
TYPE_EPIC,
} from '~/issues/constants';
export const badgeState = Vue.observable({
state: '',
......@@ -18,17 +24,22 @@ const CLASSES = {
merged: 'issuable-status-badge-merged',
};
const ISSUE_ICONS = {
opened: 'issues',
locked: 'issues',
closed: 'issue-closed',
};
const MERGE_REQUEST_ICONS = {
opened: 'merge-request-open',
locked: 'merge-request-open',
closed: 'merge-request-close',
merged: 'merge',
const ICONS = {
[TYPE_EPIC]: {
opened: 'epic',
closed: 'epic-closed',
},
[TYPE_ISSUE]: {
opened: 'issues',
locked: 'issues',
closed: 'issue-closed',
},
[TYPE_MERGE_REQUEST]: {
opened: 'merge-request-open',
locked: 'merge-request-open',
closed: 'merge-request-close',
merged: 'merge',
},
};
const STATUS = {
......@@ -91,10 +102,8 @@ export default {
return STATUS[this.state];
},
badgeIcon() {
if (this.issuableType === TYPE_ISSUE) {
return ISSUE_ICONS[this.state];
}
return MERGE_REQUEST_ICONS[this.state];
const type = this.issuableType || TYPE_MERGE_REQUEST;
return ICONS[type][this.state];
},
},
created() {
......
......@@ -28,7 +28,7 @@ export default {
type: HTMLAnchorElement,
required: true,
},
projectPath: {
namespacePath: {
type: String,
required: true,
},
......@@ -65,10 +65,10 @@ export default {
query,
update: (data) => data.project.issue,
variables() {
const { projectPath, iid } = this;
const { namespacePath, iid } = this;
return {
projectPath,
projectPath: namespacePath,
iid,
};
},
......@@ -100,7 +100,7 @@ export default {
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<div>
<work-item-type-icon v-if="!$apollo.queries.issue.loading" :work-item-type="issue.type" />
<span class="gl-text-secondary">{{ `${projectPath}#${iid}` }}</span>
<span class="gl-text-secondary">{{ `${namespacePath}#${iid}` }}</span>
</div>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
......
......@@ -19,7 +19,7 @@ export default {
type: HTMLAnchorElement,
required: true,
},
projectPath: {
namespacePath: {
type: String,
required: true,
},
......@@ -76,10 +76,10 @@ export default {
query,
update: (data) => data.project.mergeRequest,
variables() {
const { projectPath, iid } = this;
const { namespacePath, iid } = this;
return {
projectPath,
projectPath: namespacePath,
iid,
};
},
......@@ -108,7 +108,7 @@ export default {
<h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5>
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<div class="gl-text-secondary">
{{ `${projectPath}!${iid}` }}
{{ `${namespacePath}!${iid}` }}
</div>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
</div>
......
......@@ -4,7 +4,7 @@ import createDefaultClient from '~/lib/graphql';
import IssuePopover from './components/issue_popover.vue';
import MRPopover from './components/mr_popover.vue';
const componentsByReferenceType = {
export const componentsByReferenceTypeMap = {
issue: IssuePopover,
work_item: IssuePopover,
merge_request: MRPopover,
......@@ -26,9 +26,10 @@ const popoverMountedAttr = 'data-popover-mounted';
* 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 handleIssuablePopoverMount = ({
export const handleIssuablePopoverMount = ({
componentsByReferenceType = componentsByReferenceTypeMap,
apolloProvider,
projectPath,
namespacePath,
title,
iid,
referenceType,
......@@ -42,7 +43,7 @@ const handleIssuablePopoverMount = ({
new PopoverComponent({
propsData: {
target,
projectPath,
namespacePath,
iid,
cachedTitle: title,
},
......@@ -53,7 +54,7 @@ const handleIssuablePopoverMount = ({
}, 200); // 200ms delay so not every mouseover triggers Popover + API Call
};
export default (elements) => {
export default (elements, issuablePopoverMount = handleIssuablePopoverMount) => {
if (elements.length > 0) {
Vue.use(VueApollo);
......@@ -63,15 +64,16 @@ export default (elements) => {
const listenerAddedAttr = 'data-popover-listener-added';
elements.forEach((el) => {
const { projectPath, iid, referenceType } = el.dataset;
const { projectPath, groupPath, iid, referenceType } = el.dataset;
const title = el.dataset.mrTitle || el.title;
const namespacePath = groupPath || projectPath;
if (!el.getAttribute(listenerAddedAttr) && projectPath && title && iid && referenceType) {
if (!el.getAttribute(listenerAddedAttr) && namespacePath && title && iid && referenceType) {
el.addEventListener('mouseenter', ({ target }) => {
if (!el.getAttribute(popoverMountedAttr)) {
handleIssuablePopoverMount({
issuablePopoverMount({
apolloProvider,
projectPath,
namespacePath,
title,
iid,
referenceType,
......
......@@ -3,6 +3,7 @@ import dateFormat from '~/lib/dateformat';
import { roundToNearestHalf } from '~/lib/utils/common_utils';
import { sanitize } from '~/lib/dompurify';
import { s__, n__, __, sprintf } from '~/locale';
import { parsePikadayDate } from './pikaday_utility';
/**
* Returns i18n month names array.
......@@ -420,3 +421,34 @@ export const formatUtcOffset = (offset) => {
* @returns {String} the UTC timezone with the offset, e.g. `[UTC+2] Berlin, [UTC 0] London`
*/
export const formatTimezone = ({ offset, name }) => `[UTC${formatUtcOffset(offset)}] ${name}`;
/**
* Returns humanized string showing date range from provided start and due dates.
*
* @param {Date} startDate
* @param {Date} dueDate
* @returns
*/
export const humanTimeframe = (startDate, dueDate) => {
const start = startDate ? parsePikadayDate(startDate) : null;
const due = dueDate ? parsePikadayDate(dueDate) : null;
if (startDate && dueDate) {
const startDateInWords = dateInWords(start, true, start.getFullYear() === due.getFullYear());
const dueDateInWords = dateInWords(due, true);
return sprintf(__('%{startDate} – %{dueDate}'), {
startDate: startDateInWords,
dueDate: dueDateInWords,
});
} else if (startDate && !dueDate) {
return sprintf(__('%{startDate} – No due date'), {
startDate: dateInWords(start, true, false),
});
} else if (!startDate && dueDate) {
return sprintf(__('No start date – %{dueDate}'), {
dueDate: dateInWords(due, true, false),
});
}
return '';
};
......@@ -9,8 +9,8 @@ import { createAlert } from '~/alert';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { issuableListTabs, DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/constants';
import { parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '~/locale';
import { humanTimeframe } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
import { transformFetchEpicFilterParams } from '../../roadmap/utils/epic_utils';
import { epicsSortOptions } from '../constants';
......@@ -178,31 +178,7 @@ export default {
return reference;
},
epicTimeframe({ startDate, dueDate }) {
const start = startDate ? parsePikadayDate(startDate) : null;
const due = dueDate ? parsePikadayDate(dueDate) : null;
if (startDate && dueDate) {
const startDateInWords = dateInWords(
start,
true,
start.getFullYear() === due.getFullYear(),
);
const dueDateInWords = dateInWords(due, true);
return sprintf(s__('Epics|%{startDate} – %{dueDate}'), {
startDate: startDateInWords,
dueDate: dueDateInWords,
});
} else if (startDate && !dueDate) {
return sprintf(s__('Epics|%{startDate} – No due date'), {
startDate: dateInWords(start, true, false),
});
} else if (!startDate && dueDate) {
return sprintf(s__('Epics|No start date – %{dueDate}'), {
dueDate: dateInWords(due, true, false),
});
}
return '';
return humanTimeframe(startDate, dueDate);
},
fetchEpicsBy(propsName, propValue) {
if (propsName === 'currentPage') {
......
<script>
import { GlIcon, GlPopover, GlSkeletonLoader, GlTooltipDirective } from '@gitlab/ui';
import { humanTimeframe } from '~/lib/utils/datetime/date_format_utility';
import StatusBox from '~/issuable/components/status_box.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { TYPE_EPIC } from '~/issues/constants';
import query from '../queries/epic.query.graphql';
export default {
TYPE_EPIC,
components: {
GlIcon,
GlPopover,
GlSkeletonLoader,
StatusBox,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
target: {
type: HTMLAnchorElement,
required: true,
},
namespacePath: {
type: String,
required: true,
},
iid: {
type: String,
required: true,
},
cachedTitle: {
type: String,
required: true,
},
},
data() {
return {
epic: {},
};
},
computed: {
loading() {
return this.$apollo.queries.epic.loading;
},
formattedTime() {
return this.timeFormatted(this.epic.createdAt);
},
title() {
return this.epic?.title || this.cachedTitle;
},
showDetails() {
return Object.keys(this.epic).length > 0;
},
showTimeframe() {
return !this.loading && Boolean(this.epicTimeframe);
},
referenceFull() {
return `${this.namespacePath}&${this.iid}`;
},
epicTimeframe() {
return humanTimeframe(this.epic.startDate, this.epic.dueDate);
},
},
apollo: {
epic: {
query,
variables() {
const { namespacePath, iid } = this;
return {
fullPath: namespacePath,
iid,
};
},
update: (data) => data.group.epic,
},
},
};
</script>
<template>
<gl-popover :target="target" boundary="viewport" placement="top" show>
<gl-skeleton-loader v-if="loading" :height="15">
<rect width="250" height="15" rx="4" />
</gl-skeleton-loader>
<div v-else-if="showDetails" class="gl-display-flex gl-align-items-center">
<status-box :issuable-type="$options.TYPE_EPIC" :initial-state="epic.state" />
<gl-icon
v-if="epic.confidential"
v-gl-tooltip
name="eye-slash"
:title="__('Confidential')"
class="gl-text-orange-500 gl-mr-2"
:aria-label="__('Confidential')"
data-testid="confidential-icon"
/>
<span class="gl-text-secondary" data-testid="created-at">
{{ __('Opened') }} <time :datetime="epic.createdAt">{{ formattedTime }}</time>
</span>
</div>
<h5 v-if="!loading" class="gl-my-3">{{ title }}</h5>
<div class="gl-text-secondary">
{{ referenceFull }}
</div>
<div
v-if="showTimeframe"
class="gl-display-flex gl-text-secondary gl-mt-2"
data-testid="epic-timeframe"
>
<gl-icon name="calendar" />
<span class="gl-ml-2">{{ epicTimeframe }}</span>
</div>
</gl-popover>
</template>
import initIssuablePopoverCE, {
handleIssuablePopoverMount as handleIssuablePopoverMountCE,
componentsByReferenceTypeMap as componentsByReferenceTypeMapCE,
} from '~/issuable/popover';
import EpicPopover from './components/epic_popover.vue';
const componentsByReferenceTypeMap = {
...componentsByReferenceTypeMapCE,
epic: EpicPopover,
};
export const handleIssuablePopoverMount = ({
componentsByReferenceType = componentsByReferenceTypeMap,
apolloProvider,
namespacePath,
title,
iid,
referenceType,
target,
}) =>
handleIssuablePopoverMountCE({
componentsByReferenceType,
apolloProvider,
namespacePath,
title,
iid,
referenceType,
target,
});
export default (elements, issuablePopoverMount = handleIssuablePopoverMount) =>
initIssuablePopoverCE(elements, issuablePopoverMount);
query epic($fullPath: ID!, $iid: ID) {
group(fullPath: $fullPath) {
id
epic(iid: $iid) {
id
iid
title
state
createdAt
confidential
reference
startDate
dueDate
}
}
}
......@@ -27,12 +27,18 @@ def url_for_object(epic, group)
urls.group_epic_url(group, epic, only_path: context[:only_path])
end
def reference_class(object_sym, tooltip: false)
super
end
def data_attributes_for(text, group, object, link_content: false, link_reference: false)
{
original: escape_html_entities(text),
link: link_content,
link_reference: link_reference,
group: group.id,
group_path: group.full_path,
iid: object.iid,
object_sym => object.id
}
end
......
......@@ -151,31 +151,6 @@ describe('EpicsListRoot', () => {
});
});
});
describe('epicTimeframe', () => {
it.each`
startDate | dueDate | returnValue
${'2021-1-1'} | ${'2021-2-28'} | ${'Jan 1 – Feb 28, 2021'}
${'2021-1-1'} | ${'2022-2-28'} | ${'Jan 1, 2021 – Feb 28, 2022'}
${'2021-1-1'} | ${null} | ${'Jan 1, 2021 – No due date'}
${null} | ${'2021-2-28'} | ${'No start date – Feb 28, 2021'}
`(
'returns string "$returnValue" when startDate is $startDate and dueDate is $dueDate',
async ({ startDate, dueDate, returnValue }) => {
createComponent({
handler: groupEpicsQueryHandler({
nodes: [
{ ...mockRawEpic, startDate, dueDate, id: 1, iid: 10 },
...mockEpics.slice(1),
],
}),
});
await waitForPromises();
expect(wrapper.findByText(returnValue).exists()).toBe(true);
},
);
});
});
describe('fetchEpicsBy', () => {
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import StatusBox from '~/issuable/components/status_box.vue';
import epicQuery from 'ee/issuable/popover/queries/epic.query.graphql';
import EpicPopover from 'ee/issuable/popover/components/epic_popover.vue';
describe('Epic Popover', () => {
const mockEpicResponse = {
data: {
group: {
id: 'gid://gitlab/Group/1',
epic: {
id: 'gid://gitlab/Epic/1',
iid: '1',
title: 'Maxime ut soluta cumque est labore id dicta atque.',
state: 'opened',
createdAt: '2022-10-11',
confidential: false,
reference: '&1',
startDate: '2022-10-31',
dueDate: '2023-09-30',
__typename: 'Epic',
},
__typename: 'Group',
},
},
};
const mockEpic = mockEpicResponse.data.group.epic;
let wrapper;
Vue.use(VueApollo);
const mountComponent = ({
queryResponse = jest.fn().mockResolvedValue(mockEpicResponse),
} = {}) => {
wrapper = shallowMountExtended(EpicPopover, {
apolloProvider: createMockApollo([[epicQuery, queryResponse]]),
propsData: {
target: document.createElement('a'),
namespacePath: 'gitlab-org',
iid: '1',
cachedTitle: 'Maxime ut soluta cumque est labore id dicta atque.',
},
});
};
const findStatusBox = () => wrapper.findComponent(StatusBox);
describe('while popover is loading', () => {
beforeEach(() => {
mountComponent();
});
it('shows skeleton-loader', () => {
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
it('does not show status-box or created timestamp', () => {
expect(findStatusBox().exists()).toBe(false);
expect(wrapper.findByTestId('created-at').exists()).toBe(false);
});
it('does not show epic title', () => {
expect(wrapper.find('h5').exists()).toBe(false);
});
it('shows epic reference', () => {
expect(wrapper.text()).toContain(`gitlab-org&${mockEpic.iid}`);
});
});
describe('when popover contents are loaded', () => {
beforeEach(async () => {
mountComponent();
await waitForPromises();
});
it('shows status-box', () => {
const statusBox = findStatusBox();
expect(statusBox.exists()).toBe(true);
expect(statusBox.props()).toEqual({
issuableType: 'epic',
initialState: mockEpic.state,
});
});
it('shows confidentiality icon when epic is confidential', async () => {
mountComponent({
queryResponse: jest.fn().mockResolvedValue({
data: {
group: {
id: 'gid://gitlab/Group/1',
__typename: 'Group',
epic: {
...mockEpic,
confidential: true,
},
},
},
}),
});
await waitForPromises();
const confidentialIcon = wrapper.findByTestId('confidential-icon');
expect(confidentialIcon.exists()).toBe(true);
expect(confidentialIcon.props()).toEqual({
ariaLabel: 'Confidential',
size: 16,
name: 'eye-slash',
});
});
it('shows created timestamp', () => {
const createdAt = wrapper.findByTestId('created-at');
expect(createdAt.exists()).toBe(true);
expect(createdAt.text()).toContain('Opened');
});
it('shows epic title and reference', () => {
const title = wrapper.find('h5');
expect(title.exists()).toBe(true);
expect(title.text()).toBe(mockEpic.title);
expect(wrapper.text()).toContain(`gitlab-org${mockEpic.reference}`);
});
it('shows epic timeframe', () => {
const timeframe = wrapper.findByTestId('epic-timeframe');
expect(timeframe.exists()).toBe(true);
expect(timeframe.findComponent(GlIcon).exists()).toBe(true);
expect(timeframe.text()).toBe('Oct 31, 2022 – Sep 30, 2023');
});
});
});
......@@ -20,7 +20,7 @@ describe('Issue Popover', () => {
apolloProvider: createMockApollo([[issueQuery, queryResponse]]),
propsData: {
target: document.createElement('a'),
projectPath: 'foo/bar',
namespacePath: 'foo/bar',
iid: '1',
cachedTitle: 'Cached title',
},
......
import { setHTMLFixture } from 'helpers/fixtures';
import * as createDefaultClient from '~/lib/graphql';
import initIssuablePopovers, * as popover from 'ee/issuable/popover/index';
createDefaultClient.default = jest.fn();
describe('initIssuablePopoversEE', () => {
let epicWithPopover;
let epicWithoutPopover;
beforeEach(() => {
setHTMLFixture(`
<div id="epicWithPopover" class="gfm-epic" title="title" data-iid="1" data-group-path="gitlab-org" data-reference-type="epic">
Epic1
</div>
<div id="epicWithoutPopover" class="gfm-epic" title="title" data-reference-type="epic">
Epic2
</div>
`);
epicWithPopover = document.querySelector('#epicWithPopover');
epicWithoutPopover = document.querySelector('#epicWithoutPopover');
});
describe('init function', () => {
beforeEach(() => {
epicWithPopover.addEventListener = jest.fn();
epicWithoutPopover.addEventListener = jest.fn();
});
it('does not add the same event listener twice', () => {
initIssuablePopovers([epicWithPopover]);
expect(epicWithPopover.addEventListener).toHaveBeenCalledTimes(1);
});
it('does not add listener if it does not have the necessary data attributes', () => {
initIssuablePopovers([epicWithoutPopover]);
expect(epicWithoutPopover.addEventListener).not.toHaveBeenCalled();
});
});
describe('mount function', () => {
beforeEach(() => {
jest.spyOn(popover, 'handleIssuablePopoverMount').mockImplementation(jest.fn());
});
it('calls popover mount function with components for an Epic', async () => {
initIssuablePopovers([epicWithPopover], popover.handleIssuablePopoverMount);
await epicWithPopover.dispatchEvent(new Event('mouseenter', { target: epicWithPopover }));
expect(popover.handleIssuablePopoverMount).toHaveBeenCalledWith(
expect.objectContaining({
apolloProvider: expect.anything(),
iid: '1',
namespacePath: 'gitlab-org',
title: 'title',
referenceType: 'epic',
target: epicWithPopover,
}),
);
});
});
});
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Banzai::Filter::References::EpicReferenceFilter do
RSpec.describe Banzai::Filter::References::EpicReferenceFilter, feature_category: :portfolio_management do
include FilterSpecHelper
let(:urls) { Gitlab::Routing.url_helpers }
......@@ -42,7 +42,7 @@ def doc(reference = nil)
end
it 'includes default classes' do
expect(doc.css('a').first.attr('class')).to eq('gfm gfm-epic has-tooltip')
expect(doc.css('a').first.attr('class')).to eq('gfm gfm-epic')
end
it 'includes a data-group attribute' do
......@@ -52,6 +52,20 @@ def doc(reference = nil)
expect(link.attr('data-group')).to eq(group.id.to_s)
end
it 'includes a data-group-path attribute' do
link = doc.css('a').first
expect(link).to have_attribute('data-group-path')
expect(link.attr('data-group-path')).to eq(epic.group.full_path)
end
it 'includes a data-iid attribute' do
link = doc.css('a').first
expect(link).to have_attribute('data-iid')
expect(link.attr('data-iid')).to eq(epic.iid.to_s)
end
it 'includes a data-epic attribute' do
link = doc.css('a').first
......@@ -114,7 +128,7 @@ def doc(reference = nil)
end
it 'includes default classes' do
expect(doc.css('a').first.attr('class')).to eq('gfm gfm-epic has-tooltip')
expect(doc.css('a').first.attr('class')).to eq('gfm gfm-epic')
end
it 'ignores invalid epic IIDs' do
......@@ -144,7 +158,7 @@ def doc(reference = nil)
end
it 'includes default classes' do
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-epic has-tooltip')
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-epic')
end
end
......@@ -168,7 +182,7 @@ def doc(reference = nil)
end
it 'includes default classes' do
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-epic has-tooltip')
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-epic')
end
end
......@@ -199,7 +213,7 @@ def doc(reference = nil)
end
it 'includes default classes' do
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-epic has-tooltip')
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-epic')
end
end
......@@ -221,7 +235,7 @@ def doc(reference = nil)
end
it 'includes default classes' do
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-epic has-tooltip')
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-epic')
end
it 'matches link reference with trailing slash' do
......@@ -251,7 +265,7 @@ def doc(reference = nil)
end
it 'includes default classes' do
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-epic has-tooltip')
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-epic')
end
end
......@@ -275,7 +289,7 @@ def doc(reference = nil)
end
it 'includes default classes' do
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-epic has-tooltip')
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-epic')
end
end
......
......@@ -1120,6 +1120,12 @@ msgstr ""
msgid "%{spanStart}in%{spanEnd} %{errorFn}"
msgstr ""
 
msgid "%{startDate} – %{dueDate}"
msgstr ""
msgid "%{startDate} – No due date"
msgstr ""
msgid "%{start} to %{end}"
msgstr ""
 
......@@ -17714,12 +17720,6 @@ msgstr ""
msgid "Epics, issues, and merge requests"
msgstr ""
 
msgid "Epics|%{startDate} – %{dueDate}"
msgstr ""
msgid "Epics|%{startDate} – No due date"
msgstr ""
msgid "Epics|Add a new epic"
msgstr ""
 
......@@ -17735,9 +17735,6 @@ msgstr ""
msgid "Epics|Leave empty to inherit from milestone dates"
msgstr ""
 
msgid "Epics|No start date – %{dueDate}"
msgstr ""
msgid "Epics|Remove epic"
msgstr ""
 
......@@ -30792,6 +30789,9 @@ msgstr ""
msgid "No starrers matched your search"
msgstr ""
 
msgid "No start date – %{dueDate}"
msgstr ""
msgid "No suggestions found"
msgstr ""
 
......@@ -18,6 +18,8 @@ describe('Merge request status box component', () => {
${'merge_request'} | ${'Merged'} | ${'merged'} | ${'issuable-status-badge-merged'} | ${'info'} | ${'merge'}
${'issue'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'issues'}
${'issue'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'issue-closed'}
${'epic'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'epic'}
${'epic'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'epic-closed'}
`(
'with issuableType set to "$issuableType" and state set to "$initialState"',
({ issuableType, badgeText, initialState, badgeClass, badgeVariant, badgeIcon }) => {
......
......@@ -26,7 +26,7 @@ describe('Issue Popover', () => {
apolloProvider: createMockApollo([[issueQuery, queryResponse]]),
propsData: {
target: document.createElement('a'),
projectPath: 'foo/bar',
namespacePath: 'foo/bar',
iid: '1',
cachedTitle: 'Cached title',
},
......
......@@ -64,7 +64,7 @@ describe('MR Popover', () => {
apolloProvider: createMockApollo([[mergeRequestQuery, queryResponse]]),
propsData: {
target: document.createElement('a'),
projectPath: 'foo/bar',
namespacePath: 'foo/bar',
iid: '1',
cachedTitle: 'Cached Title',
},
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment