Commit 9a38c2dc authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'origin/master' into ce-to-ee-2017-05-10

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents bfdcba75 ceeb55df
Please view this file on the master branch, on stable branches it's out of date.
## 9.1.4 (2017-05-12)
- Remove warning about protecting Service Desk email from form.
- Backfill projects where the last attempt to backfill failed.
## 9.1.3 (2017-05-05)
- No changes.
......
......@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 9.1.4 (2017-05-12)
- Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123)
- Sort the network graph both by commit date and topographically. !11057
- Fix cross referencing for private and internal projects. !11243
- Handle incoming emails from aliases correctly.
- Gracefully handle failures for incoming emails which do not match on the To header, and have no References header.
- Add missing project attributes to Import/Export.
- Fixed search terms not correctly highlighting.
- Fixed bug where merge request JSON would be displayed.
## 9.1.3 (2017-05-05)
- Do not show private groups on subgroups page if user doesn't have access to.
......
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
export default {
props: {
pipeline: {
type: Object,
required: true,
},
},
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
svg: svgsDictionary[this.pipeline.details.status.icon],
};
},
computed: {
cssClasses() {
return `ci-status ci-${this.pipeline.details.status.group}`;
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
content() {
return `${this.svg} ${this.pipeline.details.status.text}`;
},
},
template: `
<td class="commit-link">
<a
:class="cssClasses"
:href="detailsPath"
v-html="content">
</a>
</td>
`,
};
/* global Flash */
import '~/lib/utils/datetime_utility';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
import MemoryUsage from './mr_widget_memory_usage';
import MRWidgetService from '../services/mr_widget_service';
......@@ -16,7 +16,7 @@ export default {
},
computed: {
svg() {
return statusClassToSvgMap.icon_status_success;
return statusIconEntityMap.icon_status_success;
},
},
methods: {
......
import PipelineStage from '../../pipelines/components/stage';
import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
import PipelineStage from '../../pipelines/components/stage.vue';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
export default {
name: 'MRWidgetPipeline',
......@@ -9,7 +9,7 @@ export default {
},
components: {
'pipeline-stage': PipelineStage,
'pipeline-status-icon': pipelineStatusIcon,
ciIcon,
},
computed: {
hasCIError() {
......@@ -18,11 +18,17 @@ export default {
return hasCI && !ciStatus;
},
svg() {
return statusClassToSvgMap.icon_status_failed;
return statusIconEntityMap.icon_status_failed;
},
stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
},
status() {
return this.mr.pipeline.details.status || {};
},
statusPath() {
return this.status ? this.status.details_path : '';
},
},
template: `
<div class="mr-widget-heading">
......@@ -38,7 +44,13 @@ export default {
<span>Could not connect to the CI server. Please check your settings and try again.</span>
</template>
<template v-else>
<pipeline-status-icon :pipelineStatus="mr.pipelineDetailedStatus" />
<div>
<a
class="icon-link"
:href="statusPath">
<ci-icon :status="status" />
</a>
</div>
<span>
Pipeline
<a
......
......@@ -41,6 +41,7 @@ export const statusIconEntityMap = {
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
<<<<<<< HEAD
export const statusCssClasses = {
icon_status_canceled: 'canceled',
......@@ -53,3 +54,5 @@ export const statusCssClasses = {
icon_status_success: 'success',
icon_status_warning: 'warning',
};
=======
>>>>>>> origin/master
<script>
import ciIcon from './ci_icon.vue';
/**
* Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Shared between:
* - Pipelines table - first column
* - Jobs table - first column
* - Pipeline show view - header
* - Job show view - header
* - MR widget
*/
export default {
props: {
status: {
type: Object,
required: true,
},
},
components: {
ciIcon,
},
computed: {
cssClass() {
const className = this.status.group;
return className ? `ci-status ci-${this.status.group}` : 'ci-status';
},
},
};
</script>
<template>
<a
:href="status.details_path"
:class="cssClass">
<ci-icon :status="status" />
{{status.text}}
</a>
</template>
<script>
<<<<<<< HEAD
import { statusIconEntityMap, statusCssClasses } from '../../vue_shared/ci_status_icons';
=======
import { statusIconEntityMap } from '../ci_status_icons';
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
* details_path: "/gitlab-org/gitlab-ce/pipelines/8150156" // url
* group:"running" // used for CSS class
* icon: "icon_status_running" // used to render the icon
* label:"running" // used for potential tooltip
* text:"running" // text rendered
* }
*
* Used in:
* - Pipelines table Badge
* - Pipelines table mini graph
* - Pipeline graph
* - Pipeline show view badge
* - Jobs table
* - Jobs show view header
* - Jobs show view sidebar
*/
>>>>>>> origin/master
export default {
props: {
status: {
......@@ -15,7 +41,11 @@
},
cssClass() {
<<<<<<< HEAD
const status = statusCssClasses[this.status.icon];
=======
const status = this.status.group;
>>>>>>> origin/master
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
},
......
import { statusClassToSvgMap } from '../pipeline_svg_icons';
export default {
name: 'PipelineStatusIcon',
props: {
pipelineStatus: { type: Object, required: true, default: () => ({}) },
},
computed: {
svg() {
return statusClassToSvgMap[this.pipelineStatus.icon];
},
statusClass() {
return `ci-status-icon ci-status-icon-${this.pipelineStatus.group}`;
},
},
template: `
<div :class="statusClass">
<a class="icon-link" :href="pipelineStatus.details_path">
<span v-html="svg" aria-hidden="true"></span>
</a>
</div>
`,
};
......@@ -2,7 +2,7 @@
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
import PipelinesStatusComponent from '../../pipelines/components/status';
import ciBadge from './ci_badge_link.vue';
import PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
......@@ -39,7 +39,7 @@ export default {
'commit-component': CommitComponent,
'dropdown-stage': PipelinesStageComponent,
'pipeline-url': PipelinesUrlComponent,
'status-scope': PipelinesStatusComponent,
ciBadge,
'time-ago': PipelinesTimeagoComponent,
},
......@@ -197,11 +197,20 @@ export default {
return '';
},
pipelineStatus() {
if (this.pipeline.details && this.pipeline.details.status) {
return this.pipeline.details.status;
}
return {};
},
},
template: `
<tr class="commit">
<status-scope :pipeline="pipeline"/>
<td class="commit-link">
<ci-badge :status="pipelineStatus"/>
</td>
<pipeline-url :pipeline="pipeline"></pipeline-url>
......
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import canceledBorderlessSvg from 'icons/_icon_status_canceled_borderless.svg';
import createdBorderlessSvg from 'icons/_icon_status_created_borderless.svg';
import failedBorderlessSvg from 'icons/_icon_status_failed_borderless.svg';
import manualBorderlessSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingBorderlessSvg from 'icons/_icon_status_pending_borderless.svg';
import runningBorderlessSvg from 'icons/_icon_status_running_borderless.svg';
import skippedBorderlessSvg from 'icons/_icon_status_skipped_borderless.svg';
import successBorderlessSvg from 'icons/_icon_status_success_borderless.svg';
import warningBorderlessSvg from 'icons/_icon_status_warning_borderless.svg';
export const statusClassToSvgMap = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
export const statusClassToBorderlessSvgMap = {
icon_status_canceled: canceledBorderlessSvg,
icon_status_created: createdBorderlessSvg,
icon_status_failed: failedBorderlessSvg,
icon_status_manual: manualBorderlessSvg,
icon_status_pending: pendingBorderlessSvg,
icon_status_running: runningBorderlessSvg,
icon_status_skipped: skippedBorderlessSvg,
icon_status_success: successBorderlessSvg,
icon_status_warning: warningBorderlessSvg,
};
......@@ -53,6 +53,7 @@
.right-sidebar-expanded {
padding-right: 0;
z-index: 300;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
......
......@@ -206,7 +206,7 @@
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
z-index: 2;
z-index: 200;
&.right-sidebar-expanded {
width: $gutter_width;
......
......@@ -90,11 +90,6 @@
align-items: center;
padding: $gl-padding-top $gl-padding 0;
i,
svg {
margin-right: 8px;
}
svg {
position: relative;
top: 1px;
......@@ -109,9 +104,10 @@
flex-wrap: wrap;
}
.ci-status-icon > .icon-link svg {
.icon-link > .ci-status-icon > svg {
width: 22px;
height: 22px;
margin-right: 8px;
}
}
......@@ -125,6 +121,7 @@
.dropdown-menu {
margin-top: 11px;
z-index: 200;
}
.ci-action-icon-wrapper {
......@@ -698,7 +695,7 @@
&.affix {
top: 0;
left: 0;
z-index: 10;
z-index: 100;
transition: right .15s;
@media (max-width: $screen-xs-max) {
......
......@@ -4,18 +4,11 @@ class IssueAssignee < ActiveRecord::Base
belongs_to :issue
belongs_to :assignee, class_name: "User", foreign_key: :user_id
after_create :update_assignee_cache_counts
after_destroy :update_assignee_cache_counts
# EE-specific
after_create :update_elasticsearch_index
after_destroy :update_elasticsearch_index
# EE-specific
def update_assignee_cache_counts
assignee&.update_cache_counts
end
def update_elasticsearch_index
if current_application_settings.elasticsearch_indexing?
ElasticIndexerWorker.perform_async(
......
......@@ -131,7 +131,6 @@ class MergeRequest < ActiveRecord::Base
participant :assignee
after_save :keep_around_commit
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
def self.reference_prefix
'!'
......@@ -193,13 +192,6 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}"
end
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
previous_assignee&.update_cache_counts
assignee&.update_cache_counts
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
......
......@@ -961,6 +961,11 @@ class User < ActiveRecord::Base
assigned_open_issues_count(force: true)
end
def invalidate_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count'])
Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
end
def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
TodosFinder.new(self, state: :done).execute.count
......
......@@ -178,6 +178,7 @@ class IssuableBaseService < BaseService
after_create(issuable)
issuable.create_cross_references!(current_user)
execute_hooks(issuable)
issuable.assignees.each(&:invalidate_cache_counts)
end
issuable
......@@ -234,6 +235,12 @@ class IssuableBaseService < BaseService
old_assignees: old_assignees
)
if old_assignees != issuable.assignees
new_assignees = issuable.assignees.to_a
affected_assignees = (old_assignees + new_assignees) - (old_assignees & new_assignees)
affected_assignees.compact.each(&:invalidate_cache_counts)
end
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
......
......@@ -43,8 +43,9 @@ module Members
)
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
member.user.update_cache_counts
end
member.user.invalidate_cache_counts
end
end
end
---
title: Remove warning about protecting Service Desk email from form
merge_request:
author:
---
title: Backfill projects where the last attempt to backfill failed
merge_request:
author:
---
title: Handle incoming emails from aliases correctly
merge_request:
author:
---
title: Refactor all CI vue badges to use the same vue component
merge_request:
author:
---
title: Sort the network graph both by commit date and topographically
merge_request: 11057
author:
---
title: Fix error on CI/CD Settings page related to invalid pipeline trigger
merge_request: 10948
author: dosuken123
---
title: Gracefully handle failures for incoming emails which do not match on the To
header, and have no References header
merge_request:
author:
---
title: Add missing project attributes to Import/Export
merge_request:
author:
---
title: Fixed bug where merge request JSON would be displayed
merge_request:
author:
......@@ -424,6 +424,7 @@ the response we need:
```
1. Use `$.mount()` to mount the component
```javascript
// bad
new Component({
......
......@@ -19,7 +19,7 @@ describe 'Navigation bar counter', feature: true, caching: true do
issue.assignees = []
user.update_cache_counts
user.invalidate_cache_counts
Timecop.travel(3.minutes.from_now) do
visit issues_path
......@@ -35,6 +35,8 @@ describe 'Navigation bar counter', feature: true, caching: true do
merge_request.update(assignee: nil)
user.invalidate_cache_counts
Timecop.travel(3.minutes.from_now) do
visit merge_requests_path
......
require 'rails_helper'
describe 'New/edit issue', feature: true, js: true do
describe 'New/edit issue', :feature, :js do
include GitlabRoutingHelper
include ActionView::Helpers::JavaScriptHelper
include WaitForAjax
let!(:project) { create(:project) }
let!(:user) { create(:user)}
......@@ -26,6 +27,8 @@ describe 'New/edit issue', feature: true, js: true do
describe 'multiple assignees' do
before do
click_button 'Unassigned'
wait_for_ajax
end
it 'unselects other assignees when unassigned is selected' do
......@@ -65,6 +68,9 @@ describe 'New/edit issue', feature: true, js: true do
expect(find('a', text: 'Assign to me')).to be_visible
click_button 'Unassigned'
wait_for_ajax
page.within '.dropdown-menu-user' do
click_link user2.name
end
......@@ -161,13 +167,15 @@ describe 'New/edit issue', feature: true, js: true do
it 'correctly updates the selected user when changing assignee' do
click_button 'Unassigned'
wait_for_ajax
page.within '.dropdown-menu-user' do
click_link user.name
end
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user.id.to_s)
# check the ::before pseudo element to ensure checkmark icon is present
expect(before_for_selector('.dropdown-menu-selectable a.is-active')).not_to eq('')
expect(before_for_selector('.dropdown-menu-selectable a:not(.is-active)')).to eq('')
......
import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
const deploymentMockData = [
{
......@@ -46,7 +46,7 @@ describe('MRWidgetDeployment', () => {
describe('svg', () => {
it('should have the proper SVG icon', () => {
const vm = createComponent(deploymentMockData);
expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_success);
expect(vm.svg).toEqual(statusIconEntityMap.icon_status_success);
});
});
});
......
import Vue from 'vue';
import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
import mockData from '../mock_data';
......@@ -24,7 +24,7 @@ describe('MRWidgetPipeline', () => {
describe('components', () => {
it('should have components added', () => {
expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
expect(pipelineComponent.components['pipeline-status-icon']).toBeDefined();
expect(pipelineComponent.components.ciIcon).toBeDefined();
});
});
......@@ -33,7 +33,7 @@ describe('MRWidgetPipeline', () => {
it('should have the proper SVG icon', () => {
const vm = createComponent({ pipeline: mockData.pipeline });
expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_failed);
expect(vm.svg).toEqual(statusIconEntityMap.icon_status_failed);
});
});
......
import Vue from 'vue';
import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
describe('CI Badge Link Component', () => {
let CIBadge;
const statuses = {
canceled: {
text: 'canceled',
label: 'canceled',
group: 'canceled',
icon: 'icon_status_canceled',
details_path: 'status/canceled',
},
created: {
text: 'created',
label: 'created',
group: 'created',
icon: 'icon_status_created',
details_path: 'status/created',
},
failed: {
text: 'failed',
label: 'failed',
group: 'failed',
icon: 'icon_status_failed',
details_path: 'status/failed',
},
manual: {
text: 'manual',
label: 'manual action',
group: 'manual',
icon: 'icon_status_manual',
details_path: 'status/manual',
},
pending: {
text: 'pending',
label: 'pending',
group: 'pending',
icon: 'icon_status_pending',
details_path: 'status/pending',
},
running: {
text: 'running',
label: 'running',
group: 'running',
icon: 'icon_status_running',
details_path: 'status/running',
},
skipped: {
text: 'skipped',
label: 'skipped',
group: 'skipped',
icon: 'icon_status_skipped',
details_path: 'status/skipped',
},
success_warining: {
text: 'passed',
label: 'passed',
group: 'success_with_warnings',
icon: 'icon_status_warning',
details_path: 'status/warning',
},
success: {
text: 'passed',
label: 'passed',
group: 'passed',
icon: 'icon_status_success',
details_path: 'status/passed',
},
};
it('should render each status badge', () => {
CIBadge = Vue.extend(ciBadge);
Object.keys(statuses).map((status) => {
const vm = new CIBadge({
propsData: {
status: statuses[status],
},
}).$mount();
expect(vm.$el.getAttribute('href')).toEqual(statuses[status].details_path);
expect(vm.$el.textContent.trim()).toEqual(statuses[status].text);
expect(vm.$el.getAttribute('class')).toEqual(`ci-status ci-${statuses[status].group}`);
expect(vm.$el.querySelector('svg')).toBeDefined();
return vm;
});
});
});
......@@ -25,6 +25,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_success',
<<<<<<< HEAD
=======
group: 'success',
>>>>>>> origin/master
},
},
}).$mount();
......@@ -37,6 +41,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_failed',
<<<<<<< HEAD
=======
group: 'failed',
>>>>>>> origin/master
},
},
}).$mount();
......@@ -49,6 +57,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_warning',
<<<<<<< HEAD
=======
group: 'warning',
>>>>>>> origin/master
},
},
}).$mount();
......@@ -61,6 +73,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_pending',
<<<<<<< HEAD
=======
group: 'pending',
>>>>>>> origin/master
},
},
}).$mount();
......@@ -73,6 +89,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_running',
<<<<<<< HEAD
=======
group: 'running',
>>>>>>> origin/master
},
},
}).$mount();
......@@ -85,6 +105,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_created',
<<<<<<< HEAD
=======
group: 'created',
>>>>>>> origin/master
},
},
}).$mount();
......@@ -97,6 +121,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_skipped',
<<<<<<< HEAD
=======
group: 'skipped',
>>>>>>> origin/master
},
},
}).$mount();
......@@ -109,6 +137,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_canceled',
<<<<<<< HEAD
=======
group: 'canceled',
>>>>>>> origin/master
},
},
}).$mount();
......@@ -121,6 +153,10 @@ describe('CI Icon component', () => {
propsData: {
status: {
icon: 'icon_status_manual',
<<<<<<< HEAD
=======
group: 'manual',
>>>>>>> origin/master
},
},
}).$mount();
......
......@@ -38,46 +38,6 @@ describe Issue, models: true do
end
end
describe "before_save" do
describe "#update_cache_counts when an issue is reassigned" do
let(:issue) { create(:issue) }
let(:assignee) { create(:user) }
context "when previous assignee exists" do
before do
issue.project.team << [assignee, :developer]
issue.assignees << assignee
end
it "updates cache counts for new assignee" do
user = create(:user)
expect(user).to receive(:update_cache_counts)
issue.assignees << user
end
it "updates cache counts for previous assignee" do
issue.assignees.first
expect_any_instance_of(User).to receive(:update_cache_counts)
issue.assignees.destroy_all
end
end
context "when previous assignee does not exist" do
it "updates cache count for the new assignee" do
issue.assignees = []
expect_any_instance_of(User).to receive(:update_cache_counts)
issue.assignees << assignee
end
end
end
end
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
......
......@@ -88,48 +88,6 @@ describe MergeRequest, models: true do
end
end
describe "before_save" do
describe "#update_cache_counts when a merge request is reassigned" do
let(:project) { create :project }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:assignee) { create :user }
context "when previous assignee exists" do
before do
project.team << [assignee, :developer]
merge_request.update(assignee: assignee)
end
it "updates cache counts for new assignee" do
user = create(:user)
expect(user).to receive(:update_cache_counts)
merge_request.update(assignee: user)
end
it "updates cache counts for previous assignee" do
old_assignee = merge_request.assignee
allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
expect(old_assignee).to receive(:update_cache_counts)
merge_request.update(assignee: nil)
end
end
context "when previous assignee does not exist" do
it "updates cache count for the new assignee" do
merge_request.update(assignee: nil)
expect_any_instance_of(User).to receive(:update_cache_counts)
merge_request.update(assignee: assignee)
end
end
end
end
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
......
......@@ -118,6 +118,22 @@ describe Issues::CreateService, services: true do
end
end
context 'when assignee is set' do
let(:opts) do
{ title: 'Title',
description: 'Description',
assignees: [assignee] }
end
it 'invalidates open issues counter for assignees when issue is assigned' do
project.team << [assignee, :master]
described_class.new(project, user, opts).execute
expect(assignee.assigned_open_issues_count).to eq 1
end
end
it 'executes issue hooks when issue is not confidential' do
opts = { title: 'Title', description: 'Description', confidential: false }
......
......@@ -59,6 +59,13 @@ describe Issues::UpdateService, services: true do
expect(issue.due_date).to eq Date.tomorrow
end
it 'updates open issue counter for assignees when issue is reassigned' do
update_issue(assignee_ids: [user2.id])
expect(user3.assigned_open_issues_count).to eq 0
expect(user2.assigned_open_issues_count).to eq 1
end
it 'sorts issues as specified by parameters' do
issue1 = create(:issue, project: project, assignees: [user3])
issue2 = create(:issue, project: project, assignees: [user3])
......
......@@ -144,6 +144,26 @@ describe MergeRequests::CreateService, services: true do
expect(merge_request.assignee).to eq(assignee)
end
context 'when assignee is set' do
let(:opts) do
{
title: 'Title',
description: 'Description',
assignee_id: assignee.id,
source_branch: 'feature',
target_branch: 'master'
}
end
it 'invalidates open merge request counter for assignees when merge request is assigned' do
project.team << [assignee, :master]
described_class.new(project, user, opts).execute
expect(assignee.assigned_open_merge_requests_count).to eq 1
end
end
context "when issuable feature is private" do
before do
project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
......
......@@ -299,6 +299,15 @@ describe MergeRequests::UpdateService, services: true do
end
end
context 'when the assignee changes' do
it 'updates open merge request counter for assignees when merge request is reassigned' do
update_merge_request(assignee_id: user2.id)
expect(user3.assigned_open_merge_requests_count).to eq 0
expect(user2.assigned_open_merge_requests_count).to eq 1
end
end
context 'when the target branch change' do
before do
update_merge_request({ target_branch: 'target' })
......
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