Commit 0ddd2914 authored by Clement Ho's avatar Clement Ho

Merge branch '9688-fe-mr-merge-order' into 'master'

FE display for "Specify that an MR must be merged after another MR"

See merge request gitlab-org/gitlab-ee!12357
parents b2c675a7 007a220b
......@@ -52,11 +52,21 @@ export default {
required: false,
default: '',
},
showReportSectionStatus: {
showReportSectionStatusIcon: {
type: Boolean,
required: false,
default: true,
},
issuesUlElementClass: {
type: String,
required: false,
default: '',
},
issueItemClass: {
type: String,
required: false,
default: null,
},
},
computed: {
issuesWithState() {
......@@ -67,6 +77,9 @@ export default {
...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)),
];
},
wclass() {
return `report-block-list ${this.issuesUlElementClass}`;
},
},
};
</script>
......@@ -77,7 +90,7 @@ export default {
:size="$options.typicalReportItemHeight"
class="report-block-container"
wtag="ul"
wclass="report-block-list"
:wclass="wclass"
>
<report-item
v-for="(wrapped, index) in issuesWithState"
......@@ -86,7 +99,8 @@ export default {
:status="wrapped.status"
:component="component"
:is-new="wrapped.isNew"
:show-report-section-status="showReportSectionStatus"
:show-report-section-status-icon="showReportSectionStatusIcon"
:class="issueItemClass"
/>
</smart-virtual-list>
</template>
......@@ -3,10 +3,7 @@ import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
import IssuesList from './issues_list.vue';
const LOADING = 'LOADING';
const ERROR = 'ERROR';
const SUCCESS = 'SUCCESS';
import { status } from '../constants';
export default {
name: 'ReportSection',
......@@ -42,7 +39,8 @@ export default {
},
successText: {
type: String,
required: true,
required: false,
default: '',
},
unresolvedIssues: {
type: Array,
......@@ -78,6 +76,21 @@ export default {
required: false,
default: true,
},
issuesUlElementClass: {
type: String,
required: false,
default: undefined,
},
issuesListContainerClass: {
type: String,
required: false,
default: undefined,
},
issueItemClass: {
type: String,
required: false,
default: undefined,
},
},
data() {
......@@ -91,13 +104,13 @@ export default {
return this.isCollapsed ? __('Expand') : __('Collapse');
},
isLoading() {
return this.status === LOADING;
return this.status === status.LOADING;
},
loadingFailed() {
return this.status === ERROR;
return this.status === status.ERROR;
},
isSuccess() {
return this.status === SUCCESS;
return this.status === status.SUCCESS;
},
isCollapsible() {
return !this.alwaysOpen && this.hasIssues;
......@@ -132,6 +145,15 @@ export default {
hasPopover() {
return Object.keys(this.popoverOptions).length > 0;
},
slotName() {
if (this.isSuccess) {
return 'success';
} else if (this.isLoading) {
return 'loading';
}
return 'error';
},
},
methods: {
toggleCollapsed() {
......@@ -147,6 +169,7 @@ export default {
<div class="media-body d-flex flex-align-self-center">
<span class="js-code-text code-text">
{{ headerText }}
<slot :name="slotName"></slot>
<popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" />
</span>
......@@ -172,6 +195,9 @@ export default {
:neutral-issues="neutralIssues"
:component="component"
:show-report-section-status-icon="showReportSectionStatusIcon"
:issues-ul-element-class="issuesUlElementClass"
:class="issuesListContainerClass"
:issue-item-class="issueItemClass"
/>
</slot>
</div>
......
......@@ -16,3 +16,9 @@ export const STATUS_NEUTRAL = 'neutral';
export const ICON_WARNING = 'warning';
export const ICON_SUCCESS = 'success';
export const ICON_NOTFOUND = 'notfound';
export const status = {
LOADING: 'LOADING',
ERROR: 'ERROR',
SUCCESS: 'SUCCESS',
};
......@@ -24,6 +24,11 @@ export default {
required: false,
default: false,
},
greyLinkWhenMerged: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
stateTitle() {
......@@ -36,6 +41,11 @@ export default {
},
);
},
issueableLinkClass() {
return this.greyLinkWhenMerged
? `sortable-link ${this.state === 'merged' ? ' text-secondary' : ''}`
: 'sortable-link';
},
},
};
</script>
......@@ -69,7 +79,7 @@ export default {
class="confidential-icon append-right-4 align-self-baseline align-self-md-auto mt-xl-0"
:aria-label="__('Confidential')"
/>
<a :href="computedPath" class="sortable-link">{{ title }}</a>
<a :href="computedPath" :class="issueableLinkClass">{{ title }}</a>
</div>
<div class="item-meta d-flex flex-wrap mt-xl-0 justify-content-xl-end flex-xl-nowrap">
<div
......
......@@ -7,11 +7,15 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
private
# rubocop: disable CodeReuse/ActiveRecord
def merge_request
@issuable = @merge_request ||= @project.merge_requests.includes(author: :status).find_by!(iid: params[:id])
@issuable =
@merge_request ||=
merge_request_includes(@project.merge_requests).find_by_iid!(params[:id])
end
def merge_request_includes(association)
association.includes(:metrics, :assignees, author: :status) # rubocop:disable CodeReuse/ActiveRecord
end
# rubocop: enable CodeReuse/ActiveRecord
def merge_request_params
params.require(:merge_request).permit(merge_request_params_attributes)
......
<script>
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { n__ } from '~/locale';
export default {
name: 'BlockingMergeRequestsBody',
components: { RelatedIssuableItem, Icon },
props: {
issue: {
type: Object,
required: true,
},
status: {
type: String,
required: true,
},
isNew: {
type: Boolean,
required: true,
},
},
computed: {
hiddenBlockingMRsText() {
return n__(
"%d merge request that you don't have access to.",
"%d merge requests that you don't have access to.",
this.issue.hiddenCount,
);
},
},
};
</script>
<template>
<div v-if="issue.hiddenCount" class="p-3 d-flex align-items-center">
<icon class="append-right-8" name="eye-slash" aria-hidden="true" />
{{ hiddenBlockingMRsText }}
</div>
<related-issuable-item
v-else
:id-key="issue.id"
:display-reference="issue.reference"
:title="issue.title"
:milestone="issue.milestone"
:assignees="issue.assignees"
:created-at="issue.created_at"
:closed-at="issue.closed_at"
:merged-at="issue.merged_at"
:path="issue.web_url"
:state="issue.state"
:is-merge-request="true"
:pipeline-status="issue.head_pipeline && issue.head_pipeline.detailed_status"
path-id-separator="!"
:class="{ 'mr-merged': issue.state === 'merged' }"
:grey-link-when-merged="true"
class="w-100 p-xl-3"
/>
</template>
<script>
import ReportSection from '~/reports/components/report_section.vue';
import { componentNames } from 'ee/vue_shared/components/reports/issue_body';
import { status as reportStatus } from '~/reports/constants';
import { n__ } from '~/locale';
export default {
name: 'BlockingMergeRequestsReport',
components: { ReportSection },
props: {
mr: {
type: Object,
required: true,
},
},
computed: {
blockingMergeRequests() {
return this.mr.blockingMergeRequests || {};
},
visibleMergeRequests() {
return this.blockingMergeRequests.visible_merge_requests || {};
},
shouldRenderBlockingMergeRequests() {
return this.blockingMergeRequests.total_count > 0;
},
openBlockingMergeRequests() {
return this.visibleMergeRequests.opened || [];
},
closedBlockingMergeRequests() {
return this.visibleMergeRequests.closed || [];
},
mergedBlockingMergeRequests() {
return this.visibleMergeRequests.merged || [];
},
unmergedBlockingMergeRequests() {
return Object.keys(this.visibleMergeRequests)
.filter(state => state !== 'merged')
.reduce(
(unmergedBlockingMRs, state) =>
state === 'closed'
? [...this.visibleMergeRequests[state], ...unmergedBlockingMRs]
: [...unmergedBlockingMRs, ...this.visibleMergeRequests[state]],
[],
);
},
unresolvedIssues() {
return this.blockingMergeRequests.hidden_count > 0
? [
{ hiddenCount: this.blockingMergeRequests.hidden_count },
...this.unmergedBlockingMergeRequests,
]
: this.unmergedBlockingMergeRequests;
},
isBlocked() {
return (
this.blockingMergeRequests.hidden_count > 0 || this.unmergedBlockingMergeRequests.length > 0
);
},
closedCount() {
return this.closedBlockingMergeRequests.length;
},
unmergedCount() {
return this.unmergedBlockingMergeRequests.length + this.blockingMergeRequests.hidden_count;
},
blockedByText() {
if (this.closedCount > 0 && this.closedCount === this.unmergedCount) {
return n__(
'Blocked by <strong>%d closed</strong> merge request.',
'Blocked by <strong>%d closed</strong> merge requests.',
this.closedCount,
);
}
const mainText = n__(
'Blocked by %d merge request',
'Blocked by %d merge requests',
this.unmergedCount,
);
return this.closedCount > 0
? `${mainText} <strong>${n__('(%d closed)', '(%d closed)', this.closedCount)}</strong>`
: mainText;
},
status() {
return this.isBlocked ? reportStatus.ERROR : reportStatus.SUCCESS;
},
},
componentNames,
};
</script>
<template>
<report-section
v-if="shouldRenderBlockingMergeRequests"
class="mr-widget-border-top mr-report blocking-mrs-report"
:status="status"
:has-issues="true"
:unresolved-issues="unresolvedIssues"
:resolved-issues="mergedBlockingMergeRequests"
:component="$options.componentNames.BlockingMergeRequestsBody"
:show-report-section-status-icon="false"
issues-ul-element-class="content-list"
issues-list-container-class="p-0"
issue-item-class="p-0"
>
<template v-slot:success>
{{ __('No blocking merge requests ') }}
<span class="text-secondary">
{{
sprintf(__('(%{mrCount} merged)'), {
mrCount: blockingMergeRequests.total_count - unmergedBlockingMergeRequests.length,
})
}}
</span>
</template>
<template v-slot:error>
<span v-html="blockedByText"></span>
</template>
</report-section>
</template>
......@@ -5,6 +5,7 @@ import GroupedMetricsReportsApp from 'ee/vue_shared/metrics_reports/grouped_metr
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import { componentNames } from 'ee/vue_shared/components/reports/issue_body';
import MrWidgetLicenses from 'ee/vue_shared/license_management/mr_widget_license_report.vue';
import BlockingMergeRequestsReport from './components/blocking_merge_requests/blocking_merge_requests_report.vue';
import { n__, s__, __, sprintf } from '~/locale';
import CEWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
......@@ -16,6 +17,7 @@ export default {
MrWidgetLicenses,
MrWidgetApprovals,
MrWidgetGeoSecondaryNode,
BlockingMergeRequestsReport,
GroupedSecurityReportsApp,
GroupedMetricsReportsApp,
ReportSection,
......@@ -216,6 +218,7 @@ export default {
:service="service"
/>
<div class="mr-section-container mr-widget-workflow">
<blocking-merge-requests-report :mr="mr" />
<report-section
v-if="shouldRenderCodeQuality"
:status="codequalityStatus"
......
......@@ -38,6 +38,8 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.initPerformanceReport(data);
this.licenseManagement = data.license_management;
this.metricsReportsPath = data.metrics_reports_path;
this.blockingMergeRequests = data.blocking_merge_requests;
}
setData(data, isRebased) {
......
......@@ -4,6 +4,7 @@ import {
} from '~/reports/components/issue_body';
import PerformanceIssueBody from 'ee/vue_merge_request_widget/components/performance_issue_body.vue';
import CodequalityIssueBody from 'ee/vue_merge_request_widget/components/codequality_issue_body.vue';
import BlockingMergeRequestsBody from 'ee/vue_merge_request_widget/components/blocking_merge_requests/blocking_merge_request_body.vue';
import LicenseIssueBody from 'ee/vue_shared/license_management/components/license_issue_body.vue';
import SastIssueBody from 'ee/vue_shared/security_reports/components/sast_issue_body.vue';
import SastContainerIssueBody from 'ee/vue_shared/security_reports/components/sast_container_issue_body.vue';
......@@ -19,6 +20,7 @@ export const components = {
SastIssueBody,
DastIssueBody,
MetricsReportsIssueBody,
BlockingMergeRequestsBody,
};
export const componentNames = {
......@@ -30,4 +32,5 @@ export const componentNames = {
SastIssueBody: SastIssueBody.name,
DastIssueBody: DastIssueBody.name,
MetricsReportsIssueBody: MetricsReportsIssueBody.name,
BlockingMergeRequestsBody: BlockingMergeRequestsBody.name,
};
......@@ -8,6 +8,15 @@ module EE
private
def merge_request_includes(association)
super.includes( # rubocop:disable CodeReuse/ActiveRecord
blocking_merge_requests: [
:metrics, :assignees, :author, :head_pipeline, :milestone,
{ source_project: :route, target_project: :route }
]
)
end
def merge_request_params
clamp_approvals_before_merge(super)
end
......
# frozen_string_literal: true
# This entity represents a merge request that blocks another MR from being
# merged.
#
# Don't use MergeRequestWidgetEntity - it's far too easy to create a loop
class BlockingMergeRequestEntity < Grape::Entity
include ::RequestAwareEntity
expose :id
expose :iid
expose :title
expose :state
expose :reference do |blocking_mr, options|
blocking_mr.to_reference(options[:from_project])
end
expose :web_url do |blocking_mr|
merge_request_path(blocking_mr)
end
expose :head_pipeline, using: ::API::Entities::Pipeline
expose :assignees, using: ::API::Entities::UserBasic
expose :milestone, using: ::API::Entities::Milestone
expose :created_at
expose :merged_at
expose :closed_at do |blocking_mr|
blocking_mr.metrics.latest_closed_at
end
end
......@@ -171,9 +171,38 @@ module EE
expose :api_unapprove_path do |merge_request|
presenter(merge_request).api_unapprove_path
end
expose :blocking_merge_requests, if: -> (mr, _) { mr&.target_project&.feature_available?(:blocking_merge_requests) }
private
def blocking_merge_requests
visible_mrs_by_state = Hash.new { |h, k| h[k] = [] }
visible_count = 0
hidden_blocking_count = 0
object.blocking_merge_requests.each do |mr|
if can?(current_user, :read_merge_request, mr)
visible_mrs_by_state[mr.state_name] << represent_blocking_mr(mr)
visible_count += 1
elsif !mr.merged? # Ignore merged hidden MRs to make display simpler
hidden_blocking_count += 1
end
end
{
total_count: visible_count + hidden_blocking_count,
hidden_count: hidden_blocking_count,
visible_merge_requests: visible_mrs_by_state
}
end
end
private
def represent_blocking_mr(blocking_mr)
blocking_mr_options = options.merge(from_project: object.target_project)
::BlockingMergeRequestEntity.represent(blocking_mr, blocking_mr_options)
end
def head_pipeline_downloadable_path_for_report_type(file_type)
object.head_pipeline&.present(current_user: current_user)
......
---
title: When a merge request is blocked by other unmerged merge requests, display them on the show page of a merge request
merge_request: 12357
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe 'Merge Request > User views blocked MR', :js do
let(:block) { create(:merge_request_block) }
let(:blocking_mr) { block.blocking_merge_request }
let(:blocked_mr) { block.blocked_merge_request }
let(:project) { blocked_mr.target_project }
let(:user) { create(:user) }
let(:merge_button) { find('.qa-merge-button') }
before do
project.add_developer(user)
sign_in(user)
end
context 'blocking merge requests are disabled' do
before do
stub_licensed_features(blocking_merge_requests: false)
end
it 'is mergeable' do
visit project_merge_request_path(project, blocked_mr)
expect(page).to have_button('Merge', disabled: false)
end
end
context 'blocking merge requests are enabled' do
before do
stub_licensed_features(blocking_merge_requests: true)
end
context 'blocking MR is not visible' do
it 'is not mergeable' do
visit project_merge_request_path(project, blocked_mr)
expect(page).to have_content('Blocked by 1 merge request')
expect(page).to have_button('Merge', disabled: true)
click_button 'Expand'
expect(page).not_to have_content(blocking_mr.title)
expect(page).to have_content("1 merge request that you don't have access to")
end
end
context 'blocking MR is visible' do
before do
blocking_mr.target_project.add_developer(user)
end
it 'is not mergeable' do
visit project_merge_request_path(project, blocked_mr)
expect(page).to have_content('Blocked by 1 merge request')
expect(page).to have_button('Merge', disabled: true)
click_button 'Expand'
expect(page).to have_content(blocking_mr.title)
end
end
end
end
import { shallowMount } from '@vue/test-utils';
import BlockingMergeRequestBody from 'ee/vue_merge_request_widget/components/blocking_merge_requests/blocking_merge_request_body.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
describe('BlockingMergeRequestBody', () => {
it('shows hidden merge request text if hidden MRs exist', () => {
const wrapper = shallowMount(BlockingMergeRequestBody, {
propsData: {
issue: { hiddenCount: 10000000, id: 10 },
status: 'string',
isNew: true,
},
});
expect(wrapper.html()).toContain("merge requests that you don't have access to");
});
it('does not show hidden merge request if hidden MRs do not exist', () => {
const wrapper = shallowMount(BlockingMergeRequestBody, {
propsData: {
issue: {},
status: 'string',
isNew: true,
},
});
expect(wrapper.html()).not.toContain("merge requests that you don't have access to");
expect(wrapper.find(RelatedIssuableItem).exists()).toBe(true);
});
});
import { createLocalVue, shallowMount, config } from '@vue/test-utils';
import BlockingMergeRequestsReport from 'ee/vue_merge_request_widget/components/blocking_merge_requests/blocking_merge_requests_report.vue';
import ReportSection from '~/reports/components/report_section.vue';
import { status as reportStatus } from '~/reports/constants';
const localVue = createLocalVue();
describe('BlockingMergeRequestsReport', () => {
let wrapper;
let props;
// Remove these hooks once we update @vue/test-utils
// See this issue: https://github.com/vuejs/vue-test-utils/issues/973
beforeAll(() => {
config.logModifiedComponents = false;
});
afterAll(() => {
config.logModifiedComponents = true;
});
beforeEach(() => {
props = {
mr: {
blockingMergeRequests: {
total_count: 3,
hidden_count: 0,
visible_merge_requests: {
opened: [{ id: 1, state: 'opened' }],
closed: [{ id: 2, state: 'closed' }],
merged: [{ id: 3, state: 'merged' }],
},
},
},
};
});
afterEach(() => {
wrapper.destroy();
});
const createComponent = (propsData = props) => {
wrapper = shallowMount(BlockingMergeRequestsReport, {
propsData,
localVue,
});
};
it('does not render blocking merge requests report if no blocking MRs exist', () => {
props.mr.blockingMergeRequests.total_count = 0;
props.mr.blockingMergeRequests.visible_merge_requests = {};
createComponent(props);
expect(wrapper.html()).toBeUndefined();
});
it('passes merged MRs as resolved issues and anything else as unresolved ', () => {
createComponent();
const reportSectionProps = wrapper.find(ReportSection).props();
expect(reportSectionProps.resolvedIssues).toHaveLength(1);
expect(reportSectionProps.resolvedIssues[0].id).toBe(3);
});
it('passes all non "merged" MRs as unresolved issues', () => {
createComponent();
const reportSectionProps = wrapper.find(ReportSection).props();
expect(reportSectionProps.unresolvedIssues.map(issue => issue.id)).toEqual([2, 1]);
});
it('sets status to "ERROR" when there are unmerged blocking MRs', () => {
createComponent();
expect(wrapper.find(ReportSection).props().status).toBe(reportStatus.ERROR);
});
it('sets status to "SUCCESS" when all blocking MRs are merged', () => {
props.mr.blockingMergeRequests.total_count = 1;
props.mr.blockingMergeRequests.visible_merge_requests = {
merged: [{ id: 3, state: 'merged' }],
};
createComponent();
expect(wrapper.find(ReportSection).props().status).toBe(reportStatus.SUCCESS);
});
describe('blockedByText', () => {
it('contains closed information if some are closed, but not all', () => {
createComponent();
expect(wrapper.vm.blockedByText).toBe(
'Blocked by 2 merge requests <strong>(1 closed)</strong>',
);
});
it('does not contain closed information if no blocking MRs are closed', () => {
delete props.mr.blockingMergeRequests.visible_merge_requests.closed;
createComponent();
expect(wrapper.vm.blockedByText).not.toContain('closed');
});
it('states when all blocking mrs are closed', () => {
delete props.mr.blockingMergeRequests.visible_merge_requests.opened;
createComponent();
expect(wrapper.vm.blockedByText).toEqual(
'Blocked by <strong>1 closed</strong> merge request.',
);
});
});
describe('unmergedBlockingMergeRequests', () => {
it('does not include merged MRs', () => {
createComponent();
const containsMergedMRs = wrapper.vm.unmergedBlockingMergeRequests.some(
mr => mr.state === 'merged',
);
expect(containsMergedMRs).toBe(false);
});
it('puts closed MRs first', () => {
createComponent();
const closedIndex = wrapper.vm.unmergedBlockingMergeRequests.findIndex(
mr => mr.state === 'closed',
);
expect(closedIndex).toBe(0);
});
});
});
......@@ -125,4 +125,33 @@ describe('Report issue', () => {
);
});
});
describe('showReportSectionStatusIcon', () => {
it('does not render CI Status Icon when showReportSectionStatusIcon is false', () => {
vm = mountComponentWithStore(ReportIssue, {
store,
props: {
issue: parsedDast[0],
component: componentNames.DastIssueBody,
status: STATUS_SUCCESS,
showReportSectionStatusIcon: false,
},
});
expect(vm.$el.querySelectorAll('.report-block-list-icon')).toHaveLength(0);
});
it('shows status icon when unspecified', () => {
vm = mountComponentWithStore(ReportIssue, {
store,
props: {
issue: parsedDast[0],
component: componentNames.DastIssueBody,
status: STATUS_SUCCESS,
},
});
expect(vm.$el.querySelectorAll('.report-block-list-icon')).toHaveLength(1);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
describe BlockingMergeRequestEntity do
set(:merge_request) { create(:merge_request) }
set(:user) { create(:user) }
let(:web_url) { Gitlab::Routing.url_helpers.project_merge_request_path(merge_request.project, merge_request) }
let(:request) { double('request', current_user: user) }
let(:extra_options) { {} }
subject(:entity) do
options = extra_options.merge(current_user: user, request: request)
described_class.new(merge_request, options)
end
it 'exposes simple attributes' do
expect(entity.as_json).to include(
id: merge_request.id,
iid: merge_request.iid,
title: merge_request.title,
state: merge_request.state,
created_at: merge_request.created_at,
merged_at: merge_request.merged_at,
closed_at: merge_request.metrics.latest_closed_at,
web_url: web_url
)
end
describe '#reference' do
let(:other_project) { create(:project) }
subject { entity.as_json[:reference] }
it { is_expected.to eq(merge_request.to_reference) }
context 'from another project' do
let(:extra_options) { { from_project: other_project } }
it 'includes the fully-qualified reference when needed' do
is_expected.to eq(merge_request.to_reference(other_project))
end
end
end
end
......@@ -14,7 +14,7 @@ describe MergeRequestWidgetEntity do
project.add_developer(user)
end
subject do
subject(:entity) do
described_class.new(merge_request, current_user: user, request: request)
end
......@@ -227,4 +227,56 @@ describe MergeRequestWidgetEntity do
end
end
end
describe 'blocking merge requests' do
set(:merge_request_block) { create(:merge_request_block, blocked_merge_request: merge_request) }
let(:blocking_mr) { merge_request_block.blocking_merge_request }
subject { entity.as_json[:blocking_merge_requests] }
context 'feature disabled' do
before do
stub_licensed_features(blocking_merge_requests: false)
end
it 'does not have the blocking_merge_requests member' do
expect(entity.as_json).not_to include(:blocking_merge_requests)
end
end
context 'feature enabled' do
before do
stub_licensed_features(blocking_merge_requests: true)
end
it 'shows the blocking merge request if visible' do
blocking_mr.project.add_developer(user)
is_expected.to include(
hidden_count: 0,
total_count: 1,
visible_merge_requests: { opened: [kind_of(BlockingMergeRequestEntity)] }
)
end
it 'hides the blocking merge request if not visible' do
is_expected.to eq(
hidden_count: 1,
total_count: 1,
visible_merge_requests: {}
)
end
it 'does not count a merged and hidden blocking MR' do
blocking_mr.update_columns(state: 'merged')
is_expected.to eq(
hidden_count: 0,
total_count: 0,
visible_merge_requests: {}
)
end
end
end
end
......@@ -105,6 +105,11 @@ msgid_plural "%d merge requests"
msgstr[0] ""
msgstr[1] ""
msgid "%d merge request that you don't have access to."
msgid_plural "%d merge requests that you don't have access to."
msgstr[0] ""
msgstr[1] ""
msgid "%d metric"
msgid_plural "%d metrics"
msgstr[0] ""
......@@ -315,6 +320,14 @@ msgstr ""
msgid "'%{source}' is not a import source"
msgstr ""
msgid "(%d closed)"
msgid_plural "(%d closed)"
msgstr[0] ""
msgstr[1] ""
msgid "(%{mrCount} merged)"
msgstr ""
msgid "(No changes)"
msgstr ""
......@@ -1947,6 +1960,16 @@ msgstr ""
msgid "Blocked"
msgstr ""
msgid "Blocked by %d merge request"
msgid_plural "Blocked by %d merge requests"
msgstr[0] ""
msgstr[1] ""
msgid "Blocked by <strong>%d closed</strong> merge request."
msgid_plural "Blocked by <strong>%d closed</strong> merge requests."
msgstr[0] ""
msgstr[1] ""
msgid "Blog"
msgstr ""
......@@ -8677,6 +8700,9 @@ msgstr ""
msgid "No available namespaces to fork the project."
msgstr ""
msgid "No blocking merge requests "
msgstr ""
msgid "No branches found"
msgstr ""
......
......@@ -197,4 +197,44 @@ describe('Report section', () => {
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
});
});
describe('Success and Error slots', () => {
const createComponent = status => {
vm = mountComponentWithSlots(ReportSection, {
props: {
status,
hasIssues: true,
},
slots: {
success: ['This is a success'],
loading: ['This is loading'],
error: ['This is an error'],
},
});
};
it('only renders success slot when status is "SUCCESS"', () => {
createComponent('SUCCESS');
expect(vm.$el.textContent.trim()).toContain('This is a success');
expect(vm.$el.textContent.trim()).not.toContain('This is an error');
expect(vm.$el.textContent.trim()).not.toContain('This is loading');
});
it('only renders error slot when status is "ERROR"', () => {
createComponent('ERROR');
expect(vm.$el.textContent.trim()).toContain('This is an error');
expect(vm.$el.textContent.trim()).not.toContain('This is a success');
expect(vm.$el.textContent.trim()).not.toContain('This is loading');
});
it('only renders loading slot when status is "LOADING"', () => {
createComponent('LOADING');
expect(vm.$el.textContent.trim()).toContain('This is loading');
expect(vm.$el.textContent.trim()).not.toContain('This is an error');
expect(vm.$el.textContent.trim()).not.toContain('This is a success');
});
});
});
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