Commit d795539f authored by Nathan Friend's avatar Nathan Friend

Refactor release progress issue stats

This commit updates the issue stats on the release progress view by
extracting the stats into their own component. This is in preparation
for the merge request stats which will be added to the release progress
view in the near future.
parent fa71f106
<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>
......@@ -5474,7 +5474,7 @@ msgstr ""
msgid "Closed this %{quick_action_target}."
msgstr ""
msgid "Closed: %{closedIssuesCount}"
msgid "Closed: %{closed}"
msgstr ""
msgid "Closes this %{quick_action_target}."
......@@ -16592,6 +16592,9 @@ msgstr ""
msgid "Merged this merge request."
msgstr ""
msgid "Merged: %{merged}"
msgstr ""
msgid "Merges this merge request immediately."
msgstr ""
......@@ -18583,10 +18586,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