Commit a2f35805 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch 'nfriend-restructure-release-issuable-stats' into 'master'

Refactor release issue stats into its own component

See merge request gitlab-org/gitlab!46063
parents 1af30cd8 d795539f
<script>
import { GlLink, GlBadge, GlSprintf } from '@gitlab/ui';
export default {
name: 'IssuableStats',
components: {
GlLink,
GlBadge,
GlSprintf,
},
props: {
label: {
type: String,
required: true,
},
total: {
type: Number,
required: true,
},
closed: {
type: Number,
required: true,
},
merged: {
type: Number,
required: false,
default: null,
},
openPath: {
type: String,
required: false,
default: '',
},
closedPath: {
type: String,
required: false,
default: '',
},
mergedPath: {
type: String,
required: false,
default: '',
},
},
computed: {
open() {
return this.total - (this.closed + (this.merged || 0));
},
showMerged() {
return this.merged != null;
},
},
};
</script>
<template>
<div
class="gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5 js-issues-container"
>
<span class="gl-mb-2">
{{ label }}
<gl-badge variant="muted" size="sm">{{ total }}</gl-badge>
</span>
<div class="gl-display-flex">
<span class="gl-white-space-pre-wrap" data-testid="open-stat">
<gl-sprintf :message="__('Open: %{open}')">
<template #open>
<gl-link v-if="openPath" :href="openPath">{{ open }}</gl-link>
<template v-else>{{ open }}</template>
</template>
</gl-sprintf>
</span>
<template v-if="showMerged">
<span class="gl-mx-2">&bull;</span>
<span class="gl-white-space-pre-wrap" data-testid="merged-stat">
<gl-sprintf :message="__('Merged: %{merged}')">
<template #merged>
<gl-link v-if="mergedPath" :href="mergedPath">{{ merged }}</gl-link>
<template v-else>{{ merged }}</template>
</template>
</gl-sprintf>
</span>
</template>
<span class="gl-mx-2">&bull;</span>
<span class="gl-white-space-pre-wrap" data-testid="closed-stat">
<gl-sprintf :message="__('Closed: %{closed}')">
<template #closed>
<gl-link v-if="closedPath" :href="closedPath">{{ closed }}</gl-link>
<template v-else>{{ closed }}</template>
</template>
</gl-sprintf>
</span>
</div>
</div>
</template>
<script>
import {
GlProgressBar,
GlLink,
GlBadge,
GlButton,
GlTooltipDirective,
GlSprintf,
} from '@gitlab/ui';
import { GlProgressBar, GlLink, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { sum } from 'lodash';
import { __, n__, sprintf } from '~/locale';
import { MAX_MILESTONES_TO_DISPLAY } from '../constants';
import IssuableStats from './issuable_stats.vue';
export default {
name: 'ReleaseBlockMilestoneInfo',
components: {
GlProgressBar,
GlLink,
GlBadge,
GlButton,
GlSprintf,
IssuableStats,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -64,18 +57,9 @@ export default {
closedIssuesCount() {
return sum(this.allIssueStats.map(stats => stats.closed || 0));
},
openIssuesCount() {
return this.totalIssuesCount - this.closedIssuesCount;
},
milestoneLabelText() {
return n__('Milestone', 'Milestones', this.milestones.length);
},
issueCountsText() {
return sprintf(__('Open: %{open} • Closed: %{closed}'), {
open: this.openIssuesCount,
closed: this.closedIssuesCount,
});
},
milestonesToDisplay() {
return this.showAllMilestones
? this.milestones
......@@ -106,20 +90,22 @@ export default {
};
</script>
<template>
<div class="release-block-milestone-info d-flex align-items-start flex-wrap">
<div class="release-block-milestone-info gl-display-flex gl-flex-wrap">
<div
v-gl-tooltip
class="milestone-progress-bar-container js-milestone-progress-bar-container d-flex flex-column align-items-start flex-shrink-1 mr-4 mb-3"
class="milestone-progress-bar-container js-milestone-progress-bar-container gl-display-flex gl-flex-direction-column gl-mr-6 gl-mb-5"
:title="__('Closed issues')"
>
<span class="mb-2">{{ percentCompleteText }}</span>
<span class="w-100">
<span class="gl-mb-3">{{ percentCompleteText }}</span>
<span class="gl-w-full">
<gl-progress-bar :value="closedIssuesCount" :max="totalIssuesCount" variant="success" />
</span>
</div>
<div class="d-flex flex-column align-items-start mr-4 mb-3 js-milestone-list-container">
<span class="mb-1">{{ milestoneLabelText }}</span>
<div class="d-flex flex-wrap align-items-end">
<div
class="gl-display-flex gl-flex-direction-column gl-mr-6 gl-mb-5 js-milestone-list-container"
>
<span class="gl-mb-2">{{ milestoneLabelText }}</span>
<div class="gl-display-flex gl-flex-wrap gl-align-items-flex-end">
<template v-for="(milestone, index) in milestonesToDisplay">
<gl-link
:key="milestone.id"
......@@ -141,32 +127,12 @@ export default {
</template>
</div>
</div>
<div class="d-flex flex-column align-items-start flex-shrink-0 mr-4 mb-3 js-issues-container">
<span class="mb-1">
{{ __('Issues') }}
<gl-badge variant="muted" size="sm">{{ totalIssuesCount }}</gl-badge>
</span>
<div class="d-flex">
<gl-link v-if="openIssuesPath" ref="openIssuesLink" :href="openIssuesPath">
<gl-sprintf :message="__('Open: %{openIssuesCount}')">
<template #openIssuesCount>{{ openIssuesCount }}</template>
</gl-sprintf>
</gl-link>
<span v-else ref="openIssuesText">
{{ sprintf(__('Open: %{openIssuesCount}'), { openIssuesCount }) }}
</span>
<span class="mx-1">&bull;</span>
<gl-link v-if="closedIssuesPath" ref="closedIssuesLink" :href="closedIssuesPath">
<gl-sprintf :message="__('Closed: %{closedIssuesCount}')">
<template #closedIssuesCount>{{ closedIssuesCount }}</template>
</gl-sprintf>
</gl-link>
<span v-else ref="closedIssuesText">
{{ sprintf(__('Closed: %{closedIssuesCount}'), { closedIssuesCount }) }}
</span>
</div>
</div>
<issuable-stats
:label="__('Issues')"
:total="totalIssuesCount"
:closed="closedIssuesCount"
:open-path="openIssuesPath"
:closed-path="closedIssuesPath"
/>
</div>
</template>
......@@ -5477,7 +5477,7 @@ msgstr ""
msgid "Closed this %{quick_action_target}."
msgstr ""
msgid "Closed: %{closedIssuesCount}"
msgid "Closed: %{closed}"
msgstr ""
msgid "Closes this %{quick_action_target}."
......@@ -16637,6 +16637,9 @@ msgstr ""
msgid "Merged this merge request."
msgstr ""
msgid "Merged: %{merged}"
msgstr ""
msgid "Merges this merge request immediately."
msgstr ""
......@@ -18632,10 +18635,7 @@ msgstr ""
msgid "Open sidebar"
msgstr ""
msgid "Open: %{openIssuesCount}"
msgstr ""
msgid "Open: %{open} • Closed: %{closed}"
msgid "Open: %{open}"
msgstr ""
msgid "Opened"
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`~/releases/components/issuable_stats.vue matches snapshot 1`] = `
"<div class=\\"gl-display-flex gl-flex-direction-column gl-flex-shrink-0 gl-mr-6 gl-mb-5 js-issues-container\\"><span class=\\"gl-mb-2\\">
Items
<span class=\\"badge badge-muted badge-pill gl-badge sm\\"><!----> 10</span></span>
<div class=\\"gl-display-flex\\"><span data-testid=\\"open-stat\\" class=\\"gl-white-space-pre-wrap\\">Open: <a href=\\"path/to/open/items\\" class=\\"gl-link\\">1</a></span> <span class=\\"gl-mx-2\\">•</span> <span data-testid=\\"merged-stat\\" class=\\"gl-white-space-pre-wrap\\">Merged: <a href=\\"path/to/merged/items\\" class=\\"gl-link\\">7</a></span> <span class=\\"gl-mx-2\\">•</span> <span data-testid=\\"closed-stat\\" class=\\"gl-white-space-pre-wrap\\">Closed: <a href=\\"path/to/closed/items\\" class=\\"gl-link\\">2</a></span></div>
</div>"
`;
import { GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import IssuableStats from '~/releases/components/issuable_stats.vue';
describe('~/releases/components/issuable_stats.vue', () => {
let wrapper;
let defaultProps;
const createComponent = propUpdates => {
wrapper = mount(IssuableStats, {
propsData: {
...defaultProps,
...propUpdates,
},
});
};
const findOpenStatLink = () => wrapper.find('[data-testid="open-stat"]').find(GlLink);
const findMergedStatLink = () => wrapper.find('[data-testid="merged-stat"]').find(GlLink);
const findClosedStatLink = () => wrapper.find('[data-testid="closed-stat"]').find(GlLink);
beforeEach(() => {
defaultProps = {
label: 'Items',
total: 10,
closed: 2,
merged: 7,
openPath: 'path/to/open/items',
closedPath: 'path/to/closed/items',
mergedPath: 'path/to/merged/items',
};
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('matches snapshot', () => {
createComponent();
expect(wrapper.html()).toMatchSnapshot();
});
describe('when only total and closed counts are provided', () => {
beforeEach(() => {
createComponent({ merged: undefined, mergedPath: undefined });
});
it('renders a label with the total count; also, the opened count and the closed count', () => {
expect(trimText(wrapper.text())).toMatchInterpolatedText('Items 10 Open: 8 • Closed: 2');
});
});
describe('when only total, merged, and closed counts are provided', () => {
beforeEach(() => {
createComponent();
});
it('renders a label with the total count; also, the opened count, the merged count, and the closed count', () => {
expect(wrapper.text()).toMatchInterpolatedText('Items 10 Open: 1 • Merged: 7 • Closed: 2');
});
});
describe('when path parameters are provided', () => {
beforeEach(() => {
createComponent();
});
it('renders the "open" stat as a link', () => {
const link = findOpenStatLink();
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(defaultProps.openPath);
});
it('renders the "merged" stat as a link', () => {
const link = findMergedStatLink();
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(defaultProps.mergedPath);
});
it('renders the "closed" stat as a link', () => {
const link = findClosedStatLink();
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(defaultProps.closedPath);
});
});
describe('when path parameters are not provided', () => {
beforeEach(() => {
createComponent({
openPath: undefined,
closedPath: undefined,
mergedPath: undefined,
});
});
it('does not render the "open" stat as a link', () => {
expect(findOpenStatLink().exists()).toBe(false);
});
it('does not render the "merged" stat as a link', () => {
expect(findMergedStatLink().exists()).toBe(false);
});
it('does not render the "closed" stat as a link', () => {
expect(findClosedStatLink().exists()).toBe(false);
});
});
});
......@@ -187,67 +187,4 @@ describe('Release block milestone info', () => {
expectAllZeros();
});
describe('Issue links', () => {
const findOpenIssuesLink = () => wrapper.find({ ref: 'openIssuesLink' });
const findOpenIssuesText = () => wrapper.find({ ref: 'openIssuesText' });
const findClosedIssuesLink = () => wrapper.find({ ref: 'closedIssuesLink' });
const findClosedIssuesText = () => wrapper.find({ ref: 'closedIssuesText' });
describe('when openIssuePath is provided', () => {
const openIssuesPath = '/path/to/open/issues';
beforeEach(() => {
return factory({ milestones, openIssuesPath });
});
it('renders the open issues as a link', () => {
expect(findOpenIssuesLink().exists()).toBe(true);
expect(findOpenIssuesText().exists()).toBe(false);
});
it('renders the open issues link with the correct href', () => {
expect(findOpenIssuesLink().attributes().href).toBe(openIssuesPath);
});
});
describe('when openIssuePath is not provided', () => {
beforeEach(() => {
return factory({ milestones });
});
it('renders the open issues as plain text', () => {
expect(findOpenIssuesLink().exists()).toBe(false);
expect(findOpenIssuesText().exists()).toBe(true);
});
});
describe('when closedIssuePath is provided', () => {
const closedIssuesPath = '/path/to/closed/issues';
beforeEach(() => {
return factory({ milestones, closedIssuesPath });
});
it('renders the closed issues as a link', () => {
expect(findClosedIssuesLink().exists()).toBe(true);
expect(findClosedIssuesText().exists()).toBe(false);
});
it('renders the closed issues link with the correct href', () => {
expect(findClosedIssuesLink().attributes().href).toBe(closedIssuesPath);
});
});
describe('when closedIssuePath is not provided', () => {
beforeEach(() => {
return factory({ milestones });
});
it('renders the closed issues as plain text', () => {
expect(findClosedIssuesLink().exists()).toBe(false);
expect(findClosedIssuesText().exists()).toBe(true);
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment