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. 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) ## 9.1.3 (2017-05-05)
- No changes. - No changes.
......
...@@ -2,6 +2,17 @@ ...@@ -2,6 +2,17 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 9.1.3 (2017-05-05)
- Do not show private groups on subgroups page if user doesn't have access to. - 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 */ /* global Flash */
import '~/lib/utils/datetime_utility'; 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 MemoryUsage from './mr_widget_memory_usage';
import MRWidgetService from '../services/mr_widget_service'; import MRWidgetService from '../services/mr_widget_service';
...@@ -16,7 +16,7 @@ export default { ...@@ -16,7 +16,7 @@ export default {
}, },
computed: { computed: {
svg() { svg() {
return statusClassToSvgMap.icon_status_success; return statusIconEntityMap.icon_status_success;
}, },
}, },
methods: { methods: {
......
import PipelineStage from '../../pipelines/components/stage'; import PipelineStage from '../../pipelines/components/stage.vue';
import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon'; import ciIcon from '../../vue_shared/components/ci_icon.vue';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons'; import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
export default { export default {
name: 'MRWidgetPipeline', name: 'MRWidgetPipeline',
...@@ -9,7 +9,7 @@ export default { ...@@ -9,7 +9,7 @@ export default {
}, },
components: { components: {
'pipeline-stage': PipelineStage, 'pipeline-stage': PipelineStage,
'pipeline-status-icon': pipelineStatusIcon, ciIcon,
}, },
computed: { computed: {
hasCIError() { hasCIError() {
...@@ -18,11 +18,17 @@ export default { ...@@ -18,11 +18,17 @@ export default {
return hasCI && !ciStatus; return hasCI && !ciStatus;
}, },
svg() { svg() {
return statusClassToSvgMap.icon_status_failed; return statusIconEntityMap.icon_status_failed;
}, },
stageText() { stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage'; 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: ` template: `
<div class="mr-widget-heading"> <div class="mr-widget-heading">
...@@ -38,7 +44,13 @@ export default { ...@@ -38,7 +44,13 @@ export default {
<span>Could not connect to the CI server. Please check your settings and try again.</span> <span>Could not connect to the CI server. Please check your settings and try again.</span>
</template> </template>
<template v-else> <template v-else>
<pipeline-status-icon :pipelineStatus="mr.pipelineDetailedStatus" /> <div>
<a
class="icon-link"
:href="statusPath">
<ci-icon :status="status" />
</a>
</div>
<span> <span>
Pipeline Pipeline
<a <a
......
...@@ -41,6 +41,7 @@ export const statusIconEntityMap = { ...@@ -41,6 +41,7 @@ export const statusIconEntityMap = {
icon_status_success: SUCCESS_SVG, icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG, icon_status_warning: WARNING_SVG,
}; };
<<<<<<< HEAD
export const statusCssClasses = { export const statusCssClasses = {
icon_status_canceled: 'canceled', icon_status_canceled: 'canceled',
...@@ -53,3 +54,5 @@ export const statusCssClasses = { ...@@ -53,3 +54,5 @@ export const statusCssClasses = {
icon_status_success: 'success', icon_status_success: 'success',
icon_status_warning: 'warning', 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> <script>
<<<<<<< HEAD
import { statusIconEntityMap, statusCssClasses } from '../../vue_shared/ci_status_icons'; 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 { export default {
props: { props: {
status: { status: {
...@@ -15,7 +41,11 @@ ...@@ -15,7 +41,11 @@
}, },
cssClass() { cssClass() {
<<<<<<< HEAD
const status = statusCssClasses[this.status.icon]; 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}`; 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 @@ ...@@ -2,7 +2,7 @@
import AsyncButtonComponent from '../../pipelines/components/async_button.vue'; import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions'; import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts'; 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 PipelinesStageComponent from '../../pipelines/components/stage.vue';
import PipelinesUrlComponent from '../../pipelines/components/pipeline_url'; import PipelinesUrlComponent from '../../pipelines/components/pipeline_url';
import PipelinesTimeagoComponent from '../../pipelines/components/time_ago'; import PipelinesTimeagoComponent from '../../pipelines/components/time_ago';
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
'commit-component': CommitComponent, 'commit-component': CommitComponent,
'dropdown-stage': PipelinesStageComponent, 'dropdown-stage': PipelinesStageComponent,
'pipeline-url': PipelinesUrlComponent, 'pipeline-url': PipelinesUrlComponent,
'status-scope': PipelinesStatusComponent, ciBadge,
'time-ago': PipelinesTimeagoComponent, 'time-ago': PipelinesTimeagoComponent,
}, },
...@@ -197,11 +197,20 @@ export default { ...@@ -197,11 +197,20 @@ export default {
return ''; return '';
}, },
pipelineStatus() {
if (this.pipeline.details && this.pipeline.details.status) {
return this.pipeline.details.status;
}
return {};
},
}, },
template: ` template: `
<tr class="commit"> <tr class="commit">
<status-scope :pipeline="pipeline"/> <td class="commit-link">
<ci-badge :status="pipelineStatus"/>
</td>
<pipeline-url :pipeline="pipeline"></pipeline-url> <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 @@ ...@@ -53,6 +53,7 @@
.right-sidebar-expanded { .right-sidebar-expanded {
padding-right: 0; padding-right: 0;
z-index: 300;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper { &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
......
...@@ -206,7 +206,7 @@ ...@@ -206,7 +206,7 @@
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 10px 20px;
z-index: 2; z-index: 200;
&.right-sidebar-expanded { &.right-sidebar-expanded {
width: $gutter_width; width: $gutter_width;
......
...@@ -90,11 +90,6 @@ ...@@ -90,11 +90,6 @@
align-items: center; align-items: center;
padding: $gl-padding-top $gl-padding 0; padding: $gl-padding-top $gl-padding 0;
i,
svg {
margin-right: 8px;
}
svg { svg {
position: relative; position: relative;
top: 1px; top: 1px;
...@@ -109,9 +104,10 @@ ...@@ -109,9 +104,10 @@
flex-wrap: wrap; flex-wrap: wrap;
} }
.ci-status-icon > .icon-link svg { .icon-link > .ci-status-icon > svg {
width: 22px; width: 22px;
height: 22px; height: 22px;
margin-right: 8px;
} }
} }
...@@ -125,6 +121,7 @@ ...@@ -125,6 +121,7 @@
.dropdown-menu { .dropdown-menu {
margin-top: 11px; margin-top: 11px;
z-index: 200;
} }
.ci-action-icon-wrapper { .ci-action-icon-wrapper {
...@@ -698,7 +695,7 @@ ...@@ -698,7 +695,7 @@
&.affix { &.affix {
top: 0; top: 0;
left: 0; left: 0;
z-index: 10; z-index: 100;
transition: right .15s; transition: right .15s;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
......
...@@ -4,18 +4,11 @@ class IssueAssignee < ActiveRecord::Base ...@@ -4,18 +4,11 @@ class IssueAssignee < ActiveRecord::Base
belongs_to :issue belongs_to :issue
belongs_to :assignee, class_name: "User", foreign_key: :user_id belongs_to :assignee, class_name: "User", foreign_key: :user_id
after_create :update_assignee_cache_counts
after_destroy :update_assignee_cache_counts
# EE-specific # EE-specific
after_create :update_elasticsearch_index after_create :update_elasticsearch_index
after_destroy :update_elasticsearch_index after_destroy :update_elasticsearch_index
# EE-specific # EE-specific
def update_assignee_cache_counts
assignee&.update_cache_counts
end
def update_elasticsearch_index def update_elasticsearch_index
if current_application_settings.elasticsearch_indexing? if current_application_settings.elasticsearch_indexing?
ElasticIndexerWorker.perform_async( ElasticIndexerWorker.perform_async(
......
...@@ -131,7 +131,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -131,7 +131,6 @@ class MergeRequest < ActiveRecord::Base
participant :assignee participant :assignee
after_save :keep_around_commit after_save :keep_around_commit
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
def self.reference_prefix def self.reference_prefix
'!' '!'
...@@ -193,13 +192,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -193,13 +192,6 @@ class MergeRequest < ActiveRecord::Base
work_in_progress?(title) ? title : "WIP: #{title}" work_in_progress?(title) ? title : "WIP: #{title}"
end 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 # Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes def card_attributes
{ {
......
...@@ -961,6 +961,11 @@ class User < ActiveRecord::Base ...@@ -961,6 +961,11 @@ class User < ActiveRecord::Base
assigned_open_issues_count(force: true) assigned_open_issues_count(force: true)
end 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) def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
TodosFinder.new(self, state: :done).execute.count TodosFinder.new(self, state: :done).execute.count
......
...@@ -178,6 +178,7 @@ class IssuableBaseService < BaseService ...@@ -178,6 +178,7 @@ class IssuableBaseService < BaseService
after_create(issuable) after_create(issuable)
issuable.create_cross_references!(current_user) issuable.create_cross_references!(current_user)
execute_hooks(issuable) execute_hooks(issuable)
issuable.assignees.each(&:invalidate_cache_counts)
end end
issuable issuable
...@@ -234,6 +235,12 @@ class IssuableBaseService < BaseService ...@@ -234,6 +235,12 @@ class IssuableBaseService < BaseService
old_assignees: old_assignees 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) after_update(issuable)
issuable.create_new_cross_references!(current_user) issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update') execute_hooks(issuable, 'update')
......
...@@ -43,8 +43,9 @@ module Members ...@@ -43,8 +43,9 @@ module Members
) )
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
member.user.update_cache_counts
end end
member.user.invalidate_cache_counts
end end
end 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: ...@@ -424,6 +424,7 @@ the response we need:
``` ```
1. Use `$.mount()` to mount the component 1. Use `$.mount()` to mount the component
```javascript ```javascript
// bad // bad
new Component({ new Component({
......
...@@ -19,7 +19,7 @@ describe 'Navigation bar counter', feature: true, caching: true do ...@@ -19,7 +19,7 @@ describe 'Navigation bar counter', feature: true, caching: true do
issue.assignees = [] issue.assignees = []
user.update_cache_counts user.invalidate_cache_counts
Timecop.travel(3.minutes.from_now) do Timecop.travel(3.minutes.from_now) do
visit issues_path visit issues_path
...@@ -35,6 +35,8 @@ describe 'Navigation bar counter', feature: true, caching: true do ...@@ -35,6 +35,8 @@ describe 'Navigation bar counter', feature: true, caching: true do
merge_request.update(assignee: nil) merge_request.update(assignee: nil)
user.invalidate_cache_counts
Timecop.travel(3.minutes.from_now) do Timecop.travel(3.minutes.from_now) do
visit merge_requests_path visit merge_requests_path
......
require 'rails_helper' require 'rails_helper'
describe 'New/edit issue', feature: true, js: true do describe 'New/edit issue', :feature, :js do
include GitlabRoutingHelper include GitlabRoutingHelper
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
include WaitForAjax
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:user) { create(:user)} let!(:user) { create(:user)}
...@@ -26,6 +27,8 @@ describe 'New/edit issue', feature: true, js: true do ...@@ -26,6 +27,8 @@ describe 'New/edit issue', feature: true, js: true do
describe 'multiple assignees' do describe 'multiple assignees' do
before do before do
click_button 'Unassigned' click_button 'Unassigned'
wait_for_ajax
end end
it 'unselects other assignees when unassigned is selected' do it 'unselects other assignees when unassigned is selected' do
...@@ -65,6 +68,9 @@ describe 'New/edit issue', feature: true, js: true do ...@@ -65,6 +68,9 @@ describe 'New/edit issue', feature: true, js: true do
expect(find('a', text: 'Assign to me')).to be_visible expect(find('a', text: 'Assign to me')).to be_visible
click_button 'Unassigned' click_button 'Unassigned'
wait_for_ajax
page.within '.dropdown-menu-user' do page.within '.dropdown-menu-user' do
click_link user2.name click_link user2.name
end end
...@@ -161,13 +167,15 @@ describe 'New/edit issue', feature: true, js: true do ...@@ -161,13 +167,15 @@ describe 'New/edit issue', feature: true, js: true do
it 'correctly updates the selected user when changing assignee' do it 'correctly updates the selected user when changing assignee' do
click_button 'Unassigned' click_button 'Unassigned'
wait_for_ajax
page.within '.dropdown-menu-user' do page.within '.dropdown-menu-user' do
click_link user.name click_link user.name
end end
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) 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) 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 # 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.is-active')).not_to eq('')
expect(before_for_selector('.dropdown-menu-selectable a:not(.is-active)')).to eq('') expect(before_for_selector('.dropdown-menu-selectable a:not(.is-active)')).to eq('')
......
import Vue from 'vue'; import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment'; import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; 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 = [ const deploymentMockData = [
{ {
...@@ -46,7 +46,7 @@ describe('MRWidgetDeployment', () => { ...@@ -46,7 +46,7 @@ describe('MRWidgetDeployment', () => {
describe('svg', () => { describe('svg', () => {
it('should have the proper SVG icon', () => { it('should have the proper SVG icon', () => {
const vm = createComponent(deploymentMockData); 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 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 pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
import mockData from '../mock_data'; import mockData from '../mock_data';
...@@ -24,7 +24,7 @@ describe('MRWidgetPipeline', () => { ...@@ -24,7 +24,7 @@ describe('MRWidgetPipeline', () => {
describe('components', () => { describe('components', () => {
it('should have components added', () => { it('should have components added', () => {
expect(pipelineComponent.components['pipeline-stage']).toBeDefined(); expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
expect(pipelineComponent.components['pipeline-status-icon']).toBeDefined(); expect(pipelineComponent.components.ciIcon).toBeDefined();
}); });
}); });
...@@ -33,7 +33,7 @@ describe('MRWidgetPipeline', () => { ...@@ -33,7 +33,7 @@ describe('MRWidgetPipeline', () => {
it('should have the proper SVG icon', () => { it('should have the proper SVG icon', () => {
const vm = createComponent({ pipeline: mockData.pipeline }); 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', () => { ...@@ -25,6 +25,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_success', icon: 'icon_status_success',
<<<<<<< HEAD
=======
group: 'success',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
...@@ -37,6 +41,10 @@ describe('CI Icon component', () => { ...@@ -37,6 +41,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_failed', icon: 'icon_status_failed',
<<<<<<< HEAD
=======
group: 'failed',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
...@@ -49,6 +57,10 @@ describe('CI Icon component', () => { ...@@ -49,6 +57,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_warning', icon: 'icon_status_warning',
<<<<<<< HEAD
=======
group: 'warning',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
...@@ -61,6 +73,10 @@ describe('CI Icon component', () => { ...@@ -61,6 +73,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_pending', icon: 'icon_status_pending',
<<<<<<< HEAD
=======
group: 'pending',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
...@@ -73,6 +89,10 @@ describe('CI Icon component', () => { ...@@ -73,6 +89,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_running', icon: 'icon_status_running',
<<<<<<< HEAD
=======
group: 'running',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
...@@ -85,6 +105,10 @@ describe('CI Icon component', () => { ...@@ -85,6 +105,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_created', icon: 'icon_status_created',
<<<<<<< HEAD
=======
group: 'created',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
...@@ -97,6 +121,10 @@ describe('CI Icon component', () => { ...@@ -97,6 +121,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_skipped', icon: 'icon_status_skipped',
<<<<<<< HEAD
=======
group: 'skipped',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
...@@ -109,6 +137,10 @@ describe('CI Icon component', () => { ...@@ -109,6 +137,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_canceled', icon: 'icon_status_canceled',
<<<<<<< HEAD
=======
group: 'canceled',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
...@@ -121,6 +153,10 @@ describe('CI Icon component', () => { ...@@ -121,6 +153,10 @@ describe('CI Icon component', () => {
propsData: { propsData: {
status: { status: {
icon: 'icon_status_manual', icon: 'icon_status_manual',
<<<<<<< HEAD
=======
group: 'manual',
>>>>>>> origin/master
}, },
}, },
}).$mount(); }).$mount();
......
...@@ -38,46 +38,6 @@ describe Issue, models: true do ...@@ -38,46 +38,6 @@ describe Issue, models: true do
end end
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 describe '#card_attributes' do
it 'includes the author name' do it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert')) allow(subject).to receive(:author).and_return(double(name: 'Robert'))
......
...@@ -88,48 +88,6 @@ describe MergeRequest, models: true do ...@@ -88,48 +88,6 @@ describe MergeRequest, models: true do
end end
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 describe '#card_attributes' do
it 'includes the author name' do it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert')) allow(subject).to receive(:author).and_return(double(name: 'Robert'))
......
...@@ -118,6 +118,22 @@ describe Issues::CreateService, services: true do ...@@ -118,6 +118,22 @@ describe Issues::CreateService, services: true do
end end
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 it 'executes issue hooks when issue is not confidential' do
opts = { title: 'Title', description: 'Description', confidential: false } opts = { title: 'Title', description: 'Description', confidential: false }
......
...@@ -59,6 +59,13 @@ describe Issues::UpdateService, services: true do ...@@ -59,6 +59,13 @@ describe Issues::UpdateService, services: true do
expect(issue.due_date).to eq Date.tomorrow expect(issue.due_date).to eq Date.tomorrow
end 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 it 'sorts issues as specified by parameters' do
issue1 = create(:issue, project: project, assignees: [user3]) issue1 = create(:issue, project: project, assignees: [user3])
issue2 = create(:issue, project: project, assignees: [user3]) issue2 = create(:issue, project: project, assignees: [user3])
......
...@@ -144,6 +144,26 @@ describe MergeRequests::CreateService, services: true do ...@@ -144,6 +144,26 @@ describe MergeRequests::CreateService, services: true do
expect(merge_request.assignee).to eq(assignee) expect(merge_request.assignee).to eq(assignee)
end 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 context "when issuable feature is private" do
before do before do
project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE,
......
...@@ -299,6 +299,15 @@ describe MergeRequests::UpdateService, services: true do ...@@ -299,6 +299,15 @@ describe MergeRequests::UpdateService, services: true do
end end
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 context 'when the target branch change' do
before do before do
update_merge_request({ target_branch: 'target' }) 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