Commit 582436b1 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '217569-display-blocking-issue-count-on-issues-in-list' into 'master'

Display blocking issue count on issues in list

See merge request gitlab-org/gitlab!36398
parents 3926bfcc 638405ee
...@@ -85,9 +85,6 @@ export default { ...@@ -85,9 +85,6 @@ export default {
dueDateWords() { dueDateWords() {
return this.dueDate ? dateInWords(this.dueDate, true) : undefined; return this.dueDate ? dateInWords(this.dueDate, true) : undefined;
}, },
hasNoComments() {
return !this.userNotesCount;
},
isOverdue() { isOverdue() {
return this.dueDate ? this.dueDate < new Date() : false; return this.dueDate ? this.dueDate < new Date() : false;
}, },
...@@ -148,32 +145,51 @@ export default { ...@@ -148,32 +145,51 @@ export default {
time_ago: escape(getTimeago().format(this.issuable.updated_at)), time_ago: escape(getTimeago().format(this.issuable.updated_at)),
}); });
}, },
userNotesCount() {
return this.issuable.user_notes_count;
},
issuableMeta() { issuableMeta() {
return [ return [
{ {
key: 'merge-requests', key: 'merge-requests',
visible: this.issuable.merge_requests_count > 0,
value: this.issuable.merge_requests_count, value: this.issuable.merge_requests_count,
title: __('Related merge requests'), title: __('Related merge requests'),
class: 'js-merge-requests', dataTestId: 'merge-requests',
icon: 'merge-request', icon: 'merge-request',
}, },
{ {
key: 'upvotes', key: 'upvotes',
visible: this.issuable.upvotes > 0,
value: this.issuable.upvotes, value: this.issuable.upvotes,
title: __('Upvotes'), title: __('Upvotes'),
class: 'js-upvotes', dataTestId: 'upvotes',
icon: 'thumb-up', icon: 'thumb-up',
}, },
{ {
key: 'downvotes', key: 'downvotes',
visible: this.issuable.downvotes > 0,
value: this.issuable.downvotes, value: this.issuable.downvotes,
title: __('Downvotes'), title: __('Downvotes'),
class: 'js-downvotes', dataTestId: 'downvotes',
icon: 'thumb-down', icon: 'thumb-down',
}, },
{
key: 'blocking-issues',
visible: this.issuable.blocking_issues_count > 0,
value: this.issuable.blocking_issues_count,
title: __('Blocking issues'),
dataTestId: 'blocking-issues',
href: `${this.issuable.web_url}#related-issues`,
icon: 'issue-block',
},
{
key: 'comments-count',
visible: !this.isJiraIssue,
value: this.issuable.user_notes_count,
title: __('Comments'),
dataTestId: 'notes-count',
href: `${this.issuable.web_url}#notes`,
class: !this.issuable.user_notes_count ? 'no-comments' : '',
icon: 'comments',
},
]; ];
}, },
}, },
...@@ -202,6 +218,9 @@ export default { ...@@ -202,6 +218,9 @@ export default {
selected: ev.target.checked, selected: ev.target.checked,
}); });
}, },
issuableMetaComponent(href) {
return href ? 'gl-link' : 'span';
},
}, },
confidentialTooltipText: __('Confidential'), confidentialTooltipText: __('Confidential'),
...@@ -216,9 +235,9 @@ export default { ...@@ -216,9 +235,9 @@ export default {
:data-labels="labelIdsString" :data-labels="labelIdsString"
:data-url="issuable.web_url" :data-url="issuable.web_url"
> >
<div class="d-flex"> <div class="gl-display-flex">
<!-- Bulk edit checkbox --> <!-- Bulk edit checkbox -->
<div v-if="isBulkEditing" class="mr-2"> <div v-if="isBulkEditing" class="gl-mr-3">
<input <input
:checked="selected" :checked="selected"
class="selected-issuable" class="selected-issuable"
...@@ -230,7 +249,7 @@ export default { ...@@ -230,7 +249,7 @@ export default {
<!-- Issuable info container --> <!-- Issuable info container -->
<!-- Issuable main info --> <!-- Issuable main info -->
<div class="flex-grow-1"> <div class="gl-flex-grow-1">
<div class="title"> <div class="title">
<span class="issue-title-text"> <span class="issue-title-text">
<gl-icon <gl-icon
...@@ -251,7 +270,10 @@ export default { ...@@ -251,7 +270,10 @@ export default {
/> />
</gl-link> </gl-link>
</span> </span>
<span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block"> <span
v-if="issuable.has_tasks"
class="gl-ml-2 task-status gl-display-none d-sm-inline-block"
>
{{ issuable.task_status }} {{ issuable.task_status }}
</span> </span>
</div> </div>
...@@ -267,7 +289,7 @@ export default { ...@@ -267,7 +289,7 @@ export default {
{{ referencePath }} {{ referencePath }}
</span> </span>
<span data-testid="openedByMessage" class="d-none d-sm-inline-block mr-1"> <span data-testid="openedByMessage" class="gl-display-none d-sm-inline-block gl-mr-2">
&middot; &middot;
<gl-sprintf <gl-sprintf
:message="isJiraIssue ? $options.i18n.openedAgoJira : $options.i18n.openedAgo" :message="isJiraIssue ? $options.i18n.openedAgoJira : $options.i18n.openedAgo"
...@@ -291,7 +313,7 @@ export default { ...@@ -291,7 +313,7 @@ export default {
<gl-link <gl-link
v-if="issuable.milestone" v-if="issuable.milestone"
v-gl-tooltip v-gl-tooltip
class="d-none d-sm-inline-block mr-1 js-milestone" class="gl-display-none d-sm-inline-block gl-mr-2 js-milestone"
:href="milestoneLink" :href="milestoneLink"
:title="milestoneTooltipText" :title="milestoneTooltipText"
> >
...@@ -302,7 +324,7 @@ export default { ...@@ -302,7 +324,7 @@ export default {
<span <span
v-if="dueDate" v-if="dueDate"
v-gl-tooltip v-gl-tooltip
class="d-none d-sm-inline-block mr-1 js-due-date" class="gl-display-none d-sm-inline-block gl-mr-2 js-due-date"
:class="{ cred: isOverdue }" :class="{ cred: isOverdue }"
:title="__('Due date')" :title="__('Due date')"
> >
...@@ -321,7 +343,7 @@ export default { ...@@ -321,7 +343,7 @@ export default {
:title="label.name" :title="label.name"
:scoped="isScoped(label)" :scoped="isScoped(label)"
size="sm" size="sm"
class="mr-1" class="gl-mr-2"
>{{ label.name }}</gl-label >{{ label.name }}</gl-label
> >
...@@ -329,7 +351,7 @@ export default { ...@@ -329,7 +351,7 @@ export default {
v-if="hasWeight" v-if="hasWeight"
v-gl-tooltip v-gl-tooltip
:title="__('Weight')" :title="__('Weight')"
class="d-none d-sm-inline-block js-weight" class="gl-display-none d-sm-inline-block"
data-testid="weight" data-testid="weight"
> >
<gl-icon name="weight" class="align-text-bottom" /> <gl-icon name="weight" class="align-text-bottom" />
...@@ -339,43 +361,37 @@ export default { ...@@ -339,43 +361,37 @@ export default {
</div> </div>
<!-- Issuable meta --> <!-- Issuable meta -->
<div class="flex-shrink-0 d-flex flex-column align-items-end justify-content-center"> <div
<div class="controls d-flex"> class="gl-flex-shrink-0 gl-display-flex gl-flex-direction-column align-items-end gl-justify-content-center"
>
<div class="controls gl-display-flex">
<span v-if="isJiraIssue" data-testid="issuable-status">{{ issuable.status }}</span> <span v-if="isJiraIssue" data-testid="issuable-status">{{ issuable.status }}</span>
<span v-else-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span> <span v-else-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span>
<issue-assignees <issue-assignees
:assignees="issuable.assignees" :assignees="issuable.assignees"
class="align-items-center d-flex ml-2" class="gl-align-items-center gl-display-flex gl-ml-3"
:icon-size="16" :icon-size="16"
img-css-classes="mr-1" img-css-classes="gl-mr-2!"
:max-visible="4" :max-visible="4"
/> />
<template v-for="meta in issuableMeta"> <template v-for="meta in issuableMeta">
<span <span
v-if="meta.value" v-if="meta.visible"
:key="meta.key" :key="meta.key"
v-gl-tooltip v-gl-tooltip
:class="['d-none d-sm-inline-block ml-2 vertical-align-middle', meta.class]" class="gl-display-none gl-display-sm-flex gl-align-items-center gl-ml-3"
:class="meta.class"
:data-testid="meta.dataTestId"
:title="meta.title" :title="meta.title"
> >
<gl-icon v-if="meta.icon" :name="meta.icon" /> <component :is="issuableMetaComponent(meta.href)" :href="meta.href">
{{ meta.value }} <gl-icon v-if="meta.icon" :name="meta.icon" />
{{ meta.value }}
</component>
</span> </span>
</template> </template>
<gl-link
v-if="!isJiraIssue"
v-gl-tooltip
class="ml-2 js-notes"
:href="`${issuable.web_url}#notes`"
:title="__('Comments')"
:class="{ 'no-comments': hasNoComments }"
>
<gl-icon name="comments" class="gl-vertical-align-text-bottom" />
{{ userNotesCount }}
</gl-link>
</div> </div>
<div v-gl-tooltip class="issuable-updated-at" :title="updatedDateString"> <div v-gl-tooltip class="issuable-updated-at" :title="updatedDateString">
{{ updatedDateAgo }} {{ updatedDateAgo }}
......
...@@ -8,6 +8,10 @@ module EE ...@@ -8,6 +8,10 @@ module EE
prepended do prepended do
expose :weight, if: ->(issue, _) { issue.supports_weight? } expose :weight, if: ->(issue, _) { issue.supports_weight? }
expose(:blocking_issues_count) do |issue, options|
issuable_metadata.blocking_issues_count
end
end end
end end
end end
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
{ "$ref": "../../../../../../../spec/fixtures/api/schemas/public_api/v4/issues.json" }, { "$ref": "../../../../../../../spec/fixtures/api/schemas/public_api/v4/issues.json" },
{ {
"properties": { "properties": {
"blocking_issues_count": { "type": ["integer", "null"] },
"weight": { "type": ["integer", "null"] }, "weight": { "type": ["integer", "null"] },
"epic_iid": { "type": ["integer", "null"] }, "epic_iid": { "type": ["integer", "null"] },
"epic": { "epic": {
......
...@@ -101,6 +101,25 @@ RSpec.describe API::Issues, :mailer do ...@@ -101,6 +101,25 @@ RSpec.describe API::Issues, :mailer do
expect(response).to match_response_schema('public_api/v4/issues', dir: 'ee') expect(response).to match_response_schema('public_api/v4/issues', dir: 'ee')
end end
context "blocking issues count" do
it 'returns a blocking issues count of 0 if there are no blocking issues' do
get api('/issues', user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.first).to include('blocking_issues_count' => 0)
end
it 'returns a blocking issues count of 1 if there exists a blocking issue' do
blocked_issue = build(:issue, author: user2, project: project, created_at: 1.day.ago)
create(:issue_link, source: issue, target: blocked_issue, link_type: IssueLink::TYPE_BLOCKS)
get api('/issues', user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.first).to include('blocking_issues_count' => 1)
end
end
context "filtering by weight" do context "filtering by weight" do
let!(:issue1) { create(:issue, author: user2, project: project, weight: 1, created_at: 3.days.ago) } let!(:issue1) { create(:issue, author: user2, project: project, weight: 1, created_at: 3.days.ago) }
let!(:issue2) { create(:issue, author: user2, project: project, weight: 5, created_at: 2.days.ago) } let!(:issue2) { create(:issue, author: user2, project: project, weight: 5, created_at: 2.days.ago) }
......
...@@ -3777,6 +3777,9 @@ msgstr "" ...@@ -3777,6 +3777,9 @@ msgstr ""
msgid "Blocked issue" msgid "Blocked issue"
msgstr "" msgstr ""
msgid "Blocking issues"
msgstr ""
msgid "Blocks" msgid "Blocks"
msgstr "" msgstr ""
......
...@@ -85,12 +85,13 @@ describe('Issuable component', () => { ...@@ -85,12 +85,13 @@ describe('Issuable component', () => {
const findMilestoneTooltip = () => findMilestone().attributes('title'); const findMilestoneTooltip = () => findMilestone().attributes('title');
const findDueDate = () => wrapper.find('.js-due-date'); const findDueDate = () => wrapper.find('.js-due-date');
const findLabels = () => wrapper.findAll(GlLabel); const findLabels = () => wrapper.findAll(GlLabel);
const findWeight = () => wrapper.find('.js-weight'); const findWeight = () => wrapper.find('[data-testid="weight"]');
const findAssignees = () => wrapper.find(IssueAssignees); const findAssignees = () => wrapper.find(IssueAssignees);
const findMergeRequestsCount = () => wrapper.find('.js-merge-requests'); const findBlockingIssuesCount = () => wrapper.find('[data-testid="blocking-issues"]');
const findUpvotes = () => wrapper.find('.js-upvotes'); const findMergeRequestsCount = () => wrapper.find('[data-testid="merge-requests"]');
const findDownvotes = () => wrapper.find('.js-downvotes'); const findUpvotes = () => wrapper.find('[data-testid="upvotes"]');
const findNotes = () => wrapper.find('.js-notes'); const findDownvotes = () => wrapper.find('[data-testid="downvotes"]');
const findNotes = () => wrapper.find('[data-testid="notes-count"]');
const findBulkCheckbox = () => wrapper.find('input.selected-issuable'); const findBulkCheckbox = () => wrapper.find('input.selected-issuable');
const findScopedLabels = () => findLabels().filter(w => isScopedLabel({ title: w.text() })); const findScopedLabels = () => findLabels().filter(w => isScopedLabel({ title: w.text() }));
const findUnscopedLabels = () => findLabels().filter(w => !isScopedLabel({ title: w.text() })); const findUnscopedLabels = () => findLabels().filter(w => !isScopedLabel({ title: w.text() }));
...@@ -181,6 +182,7 @@ describe('Issuable component', () => { ...@@ -181,6 +182,7 @@ describe('Issuable component', () => {
${'due date'} | ${checkExists(findDueDate)} ${'due date'} | ${checkExists(findDueDate)}
${'labels'} | ${checkExists(findLabels)} ${'labels'} | ${checkExists(findLabels)}
${'weight'} | ${checkExists(findWeight)} ${'weight'} | ${checkExists(findWeight)}
${'blocking issues count'} | ${checkExists(findBlockingIssuesCount)}
${'merge request count'} | ${checkExists(findMergeRequestsCount)} ${'merge request count'} | ${checkExists(findMergeRequestsCount)}
${'upvotes'} | ${checkExists(findUpvotes)} ${'upvotes'} | ${checkExists(findUpvotes)}
${'downvotes'} | ${checkExists(findDownvotes)} ${'downvotes'} | ${checkExists(findDownvotes)}
...@@ -430,11 +432,12 @@ describe('Issuable component', () => { ...@@ -430,11 +432,12 @@ describe('Issuable component', () => {
}); });
describe.each` describe.each`
desc | key | finder desc | key | finder
${'with merge requests count'} | ${'merge_requests_count'} | ${findMergeRequestsCount} ${'with blocking issues count'} | ${'blocking_issues_count'} | ${findBlockingIssuesCount}
${'with upvote count'} | ${'upvotes'} | ${findUpvotes} ${'with merge requests count'} | ${'merge_requests_count'} | ${findMergeRequestsCount}
${'with downvote count'} | ${'downvotes'} | ${findDownvotes} ${'with upvote count'} | ${'upvotes'} | ${findUpvotes}
${'with notes count'} | ${'user_notes_count'} | ${findNotes} ${'with downvote count'} | ${'downvotes'} | ${findDownvotes}
${'with notes count'} | ${'user_notes_count'} | ${findNotes}
`('$desc', ({ key, finder }) => { `('$desc', ({ key, finder }) => {
beforeEach(() => { beforeEach(() => {
issuable[key] = TEST_META_COUNT; issuable[key] = TEST_META_COUNT;
...@@ -442,7 +445,7 @@ describe('Issuable component', () => { ...@@ -442,7 +445,7 @@ describe('Issuable component', () => {
factory({ issuable }); factory({ issuable });
}); });
it('renders merge requests count', () => { it('renders correct count', () => {
expect(finder().exists()).toBe(true); expect(finder().exists()).toBe(true);
expect(finder().text()).toBe(TEST_META_COUNT.toString()); expect(finder().text()).toBe(TEST_META_COUNT.toString());
expect(finder().classes('no-comments')).toBe(false); expect(finder().classes('no-comments')).toBe(false);
......
...@@ -18,6 +18,7 @@ export const simpleIssue = { ...@@ -18,6 +18,7 @@ export const simpleIssue = {
}, },
assignee: null, assignee: null,
user_notes_count: 0, user_notes_count: 0,
blocking_issues_count: 0,
merge_requests_count: 0, merge_requests_count: 0,
upvotes: 0, upvotes: 0,
downvotes: 0, downvotes: 0,
......
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