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