Commit e2a2e8c3 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ee-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents 0f31c32c 2722393d
...@@ -478,6 +478,7 @@ db:migrate:reset-mysql: ...@@ -478,6 +478,7 @@ db:migrate:reset-mysql:
stage: test stage: test
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
CREATE_DB_USER: "true"
script: script:
- git fetch https://gitlab.com/gitlab-org/gitlab-ee.git v9.3.0-ee - git fetch https://gitlab.com/gitlab-org/gitlab-ee.git v9.3.0-ee
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
...@@ -522,6 +523,7 @@ db:rollback-mysql: ...@@ -522,6 +523,7 @@ db:rollback-mysql:
variables: variables:
SIZE: "1" SIZE: "1"
SETUP_DB: "false" SETUP_DB: "false"
CREATE_DB_USER: "true"
script: script:
- git clone https://gitlab.com/gitlab-org/gitlab-test.git - git clone https://gitlab.com/gitlab-org/gitlab-test.git
/home/git/repositories/gitlab-org/gitlab-test.git /home/git/repositories/gitlab-org/gitlab-test.git
...@@ -557,7 +559,6 @@ gitlab:assets:compile: ...@@ -557,7 +559,6 @@ gitlab:assets:compile:
NODE_ENV: "production" NODE_ENV: "production"
RAILS_ENV: "production" RAILS_ENV: "production"
SETUP_DB: "false" SETUP_DB: "false"
USE_DB: "false"
SKIP_STORAGE_VALIDATION: "true" SKIP_STORAGE_VALIDATION: "true"
WEBPACK_REPORT: "true" WEBPACK_REPORT: "true"
NO_COMPRESSION: "true" NO_COMPRESSION: "true"
......
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.
## 10.1.3 (2017-11-10)
- [FIXED] Fix: Failed to rebase MR from forked repo.
## 10.1.2 (2017-11-08) ## 10.1.2 (2017-11-08)
- [SECURITY] Fix vulnerability that could allow any user of a Geo instance to clone any repository on the secondary instance. - [SECURITY] Fix vulnerability that could allow any user of a Geo instance to clone any repository on the secondary instance.
......
...@@ -2,6 +2,22 @@ ...@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.1.3 (2017-11-10)
- [SECURITY] Prevent OAuth phishing attack by presenting detailed wording about app to user during authorization.
- [FIXED] Fix cancel button not working while uploading on the new issue page. !15137
- [FIXED] Fix webhooks recent deliveries. !15146 (Alexander Randa (@randaalex))
- [FIXED] Fix issues with forked projects of which the source was deleted. !15150
- [FIXED] Fix GPG signature popup info in Safari and Firefox. !15228
- [FIXED] Make sure group and project creation is blocked for new users that are external by default.
- [FIXED] Fix arguments Import/Export error importing project merge requests.
- [FIXED] Fix diff parser so it tolerates to diff special markers in the content.
- [FIXED] Fix a migration that adds merge_requests_ff_only_enabled column to MR table.
- [FIXED] Render 404 when polling commit notes without having permissions.
- [FIXED] Show error message when fast-forward merge is not possible.
- [FIXED] Avoid regenerating the ref path for the environment.
- [PERFORMANCE] Remove Filesystem check metrics that use too much CPU to handle requests.
## 10.1.2 (2017-11-08) ## 10.1.2 (2017-11-08)
- [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities. - [SECURITY] Add X-Content-Type-Options header in API responses to make it more difficult to find other vulnerabilities.
......
...@@ -331,7 +331,7 @@ GitLabDropdown = (function() { ...@@ -331,7 +331,7 @@ GitLabDropdown = (function() {
if (_this.dropdown.find('.dropdown-toggle-page').length) { if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector; selector = ".dropdown-page-one " + selector;
} }
return $(selector); return $(selector, this.instance.dropdown);
}; };
})(this), })(this),
data: (function(_this) { data: (function(_this) {
......
...@@ -135,7 +135,6 @@ window.dateFormat = dateFormat; ...@@ -135,7 +135,6 @@ window.dateFormat = dateFormat;
* @param {Number} seconds * @param {Number} seconds
* @return {String} * @return {String}
*/ */
// eslint-disable-next-line import/prefer-default-export
export function timeIntervalInWords(intervalInSeconds) { export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10); const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60); const minutes = Math.floor(secondsInteger / 60);
...@@ -149,3 +148,17 @@ export function timeIntervalInWords(intervalInSeconds) { ...@@ -149,3 +148,17 @@ export function timeIntervalInWords(intervalInSeconds) {
} }
return text; return text;
} }
export function dateInWords(date, abbreviated = false) {
if (!date) return date;
const month = date.getMonth();
const year = date.getFullYear();
const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
return `${monthName} ${date.getDate()}, ${year}`;
}
...@@ -24,6 +24,10 @@ export function highCountTrim(count) { ...@@ -24,6 +24,10 @@ export function highCountTrim(count) {
return count > 99 ? '99+' : count; return count > 99 ? '99+' : count;
} }
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
gl.text.randomString = function() { gl.text.randomString = function() {
return Math.random().toString(36).substring(7); return Math.random().toString(36).substring(7);
}; };
......
<script>
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
export default {
name: 'datePicker',
props: {
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
},
methods: {
selected(dateText) {
this.$emit('newDateSelected', this.calendar.toString(dateText));
},
toggled() {
this.$emit('hidePicker');
},
},
mounted() {
this.calendar = new Pikaday({
field: this.$el.querySelector('.dropdown-menu-toggle'),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
setDefaultDate: !!this.selectedDate,
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
});
this.$el.append(this.calendar.el);
this.calendar.show();
},
beforeDestroy() {
this.calendar.destroy();
},
};
</script>
<template>
<div class="pikaday-container">
<div class="dropdown open">
<button
type="button"
class="dropdown-menu-toggle"
data-toggle="dropdown"
@click="toggled"
>
<span class="dropdown-toggle-text">
{{label}}
</span>
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button>
</div>
</div>
</template>
<script>
export default {
name: 'collapsedCalendarIcon',
props: {
containerClass: {
type: String,
required: false,
default: '',
},
text: {
type: String,
required: false,
default: '',
},
showIcon: {
type: Boolean,
required: false,
default: true,
},
},
methods: {
click() {
this.$emit('click');
},
},
};
</script>
<template>
<div
:class="containerClass"
@click="click"
>
<i
v-if="showIcon"
class="fa fa-calendar"
aria-hidden="true"
>
</i>
<slot>
<span>
{{ text }}
</span>
</slot>
</div>
</template>
<script>
import { dateInWords } from '../../../lib/utils/datetime_utility';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
export default {
name: 'sidebarCollapsedGroupedDatePicker',
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
disableClickableIcons: {
type: Boolean,
required: false,
default: false,
},
},
components: {
toggleSidebar,
collapsedCalendarIcon,
},
computed: {
hasMinAndMaxDates() {
return this.minDate && this.maxDate;
},
hasNoMinAndMaxDates() {
return !this.minDate && !this.maxDate;
},
showMinDateBlock() {
return this.minDate || this.hasNoMinAndMaxDates;
},
showFromText() {
return !this.maxDate && this.minDate;
},
iconClass() {
const disabledClass = this.disableClickableIcons ? 'disabled' : '';
return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`;
},
},
methods: {
toggleSidebar() {
this.$emit('toggleCollapse');
},
dateText(dateType = 'min') {
const date = this[`${dateType}Date`];
const dateWords = dateInWords(date, true);
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
return date ? parsedDateWords : 'None';
},
},
};
</script>
<template>
<div class="block sidebar-grouped-item">
<div
v-if="showToggleSidebar"
class="issuable-sidebar-header"
>
<toggle-sidebar
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
<collapsed-calendar-icon
v-if="showMinDateBlock"
:container-class="iconClass"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
<span v-if="showFromText">From</span>
<span>{{ dateText('min') }}</span>
</span>
</collapsed-calendar-icon>
<div
v-if="hasMinAndMaxDates"
class="text-center sidebar-collapsed-divider"
>
-
</div>
<collapsed-calendar-icon
v-if="maxDate"
:container-class="iconClass"
:show-icon="!minDate"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
<span v-if="!minDate">Until</span>
<span>{{ dateText('max') }}</span>
</span>
</collapsed-calendar-icon>
</div>
</template>
<script>
import datePicker from '../pikaday.vue';
import loadingIcon from '../loading_icon.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import { dateInWords } from '../../../lib/utils/datetime_utility';
export default {
name: 'sidebarDatePicker',
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
},
data() {
return {
editing: false,
};
},
components: {
datePicker,
toggleSidebar,
loadingIcon,
collapsedCalendarIcon,
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : 'None';
},
},
methods: {
stopEditing() {
this.editing = false;
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.date = date;
this.editing = false;
this.$emit('saveDate', date);
},
toggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
<div class="block">
<div class="issuable-sidebar-header">
<toggle-sidebar
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
<collapsed-calendar-icon
class="sidebar-collapsed-icon"
:text="collapsedText"
/>
<div class="title">
{{ label }}
<loading-icon
v-if="isLoading"
:inline="true"
/>
<div class="pull-right">
<button
v-if="editable && !editing"
type="button"
class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
@click="toggleDatePicker"
>
Edit
</button>
<toggle-sidebar
v-if="showToggleSidebar"
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
</div>
<div class="value">
<date-picker
v-if="editing"
:selected-date="selectedDate"
:min-date="minDate"
:max-date="maxDate"
:label="label"
@newDateSelected="newDateSelected"
@hidePicker="stopEditing"
/>
<span
v-else
class="value-content"
>
<template v-if="selectedDate">
<strong>{{ selectedDateWords }}</strong>
<span
v-if="selectedAndEditable"
class="no-value"
>
-
<button
type="button"
class="btn-blank btn-link btn-secondary-hover-link"
@click="newDateSelected(null)"
>
remove
</button>
</span>
</template>
<span
v-else
class="no-value"
>
None
</span>
</span>
</div>
</div>
</template>
<script>
export default {
name: 'toggleSidebar',
props: {
collapsed: {
type: Boolean,
required: true,
},
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
</script>
<template>
<button
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
>
<i
aria-label="toggle collapse"
class="fa"
:class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }"
></i>
</button>
</template>
...@@ -412,6 +412,7 @@ ...@@ -412,6 +412,7 @@
padding: 0; padding: 0;
background: transparent; background: transparent;
border: 0; border: 0;
border-radius: 0;
&:hover, &:hover,
&:active, &:active,
...@@ -421,3 +422,25 @@ ...@@ -421,3 +422,25 @@
box-shadow: none; box-shadow: none;
} }
} }
.btn-link.btn-secondary-hover-link {
color: $gl-text-color-secondary;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
}
.btn-link.btn-primary-hover-link {
color: inherit;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
}
...@@ -43,11 +43,13 @@ ...@@ -43,11 +43,13 @@
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
cursor: pointer;
.btn { .btn {
background-color: $gray-light; background-color: $gray-light;
} }
&:not(.disabled) {
cursor: pointer;
}
} }
} }
...@@ -55,6 +57,10 @@ ...@@ -55,6 +57,10 @@
padding-right: 0; padding-right: 0;
z-index: 300; z-index: 300;
.btn-sidebar-action {
display: inline-flex;
}
@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):not(.issuable-bulk-update-sidebar) .content-wrapper { &:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width; padding-right: $gutter_collapsed_width;
...@@ -136,3 +142,18 @@ ...@@ -136,3 +142,18 @@
.issuable-sidebar { .issuable-sidebar {
@include new-style-dropdown; @include new-style-dropdown;
} }
.pikaday-container {
.pika-single {
margin-top: 2px;
width: 250px;
}
.dropdown-menu-toggle {
line-height: 20px;
}
}
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
...@@ -284,10 +284,15 @@ ...@@ -284,10 +284,15 @@
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
} }
.no-value { .no-value,
.btn-secondary-hover-link {
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.btn-secondary-hover-link:hover {
color: $gl-link-color;
}
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
display: none; display: none;
} }
...@@ -353,7 +358,8 @@ ...@@ -353,7 +358,8 @@
.gutter-toggle { .gutter-toggle {
width: 100%; width: 100%;
margin-left: 0; margin-left: 0;
padding-left: 25px; padding-left: 0;
text-align: center;
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
...@@ -367,7 +373,7 @@ ...@@ -367,7 +373,7 @@
fill: $issuable-sidebar-color; fill: $issuable-sidebar-color;
} }
&:hover, &:hover:not(.disabled),
&:hover .todo-undone { &:hover .todo-undone {
color: $gl-text-color; color: $gl-text-color;
...@@ -953,3 +959,21 @@ ...@@ -953,3 +959,21 @@
.add-issuable-form-actions { .add-issuable-form-actions {
margin-top: $gl-padding; margin-top: $gl-padding;
} }
.right-sidebar-collapsed {
.sidebar-grouped-item {
.sidebar-collapsed-icon {
margin-bottom: 0;
}
.sidebar-collapsed-divider {
line-height: 5px;
font-size: 12px;
color: $theme-gray-700;
+ .sidebar-collapsed-icon {
padding-top: 0;
}
}
}
}
...@@ -11,7 +11,8 @@ module NavHelper ...@@ -11,7 +11,8 @@ module NavHelper
if current_path?('merge_requests#show') || if current_path?('merge_requests#show') ||
current_path?('projects/merge_requests/conflicts#show') || current_path?('projects/merge_requests/conflicts#show') ||
current_path?('issues#show') || current_path?('issues#show') ||
current_path?('milestones#show') current_path?('milestones#show') ||
current_path?('epics#show')
if cookies[:collapsed_gutter] == 'true' if cookies[:collapsed_gutter] == 'true'
%w[page-gutter right-sidebar-collapsed] %w[page-gutter right-sidebar-collapsed]
else else
......
...@@ -6,6 +6,8 @@ module MergeRequests ...@@ -6,6 +6,8 @@ module MergeRequests
# Executed when you do merge via GitLab UI # Executed when you do merge via GitLab UI
# #
class MergeService < MergeRequests::BaseService class MergeService < MergeRequests::BaseService
prepend EE::MergeRequests::MergeService
MergeError = Class.new(StandardError) MergeError = Class.new(StandardError)
attr_reader :merge_request, :source attr_reader :merge_request, :source
...@@ -18,17 +20,7 @@ module MergeRequests ...@@ -18,17 +20,7 @@ module MergeRequests
@merge_request = merge_request @merge_request = merge_request
unless @merge_request.mergeable? error_check!
return handle_merge_error(log_message: 'Merge request is not mergeable', save_message_on_model: true)
end
check_size_limit
@source = find_merge_source
unless @source
return handle_merge_error(log_message: 'No source for merge', save_message_on_model: true)
end
merge_request.in_locked_state do merge_request.in_locked_state do
if commit if commit
...@@ -65,6 +57,19 @@ module MergeRequests ...@@ -65,6 +57,19 @@ module MergeRequests
private private
def error_check!
error =
if @merge_request.should_be_rebased?
'Only fast-forward merge is allowed for your project. Please update your source branch'
elsif !@merge_request.mergeable?
'Merge request is not mergeable'
elsif !source
'No source for merge'
end
raise MergeError, error if error
end
def commit def commit
message = params[:commit_message] || merge_request.merge_commit_message message = params[:commit_message] || merge_request.merge_commit_message
...@@ -115,25 +120,8 @@ module MergeRequests ...@@ -115,25 +120,8 @@ module MergeRequests
merge_request.to_reference(full: true) merge_request.to_reference(full: true)
end end
def check_size_limit def source
if @merge_request.target_project.above_size_limit? @source ||= @merge_request.diff_head_sha
message = Gitlab::RepositorySizeError.new(@merge_request.target_project).merge_error
raise MergeError, message
end
end
def find_merge_source
return merge_request.diff_head_sha unless merge_request.squash
squash_result = SquashService.new(project, current_user, params).execute(merge_request)
case squash_result[:status]
when :success
squash_result[:squash_sha]
when :error
raise MergeError, squash_result[:message]
end
end end
end end
end end
--- ---
title: 'Fix: Failed to rebase MR from forked repo' title: Add sidebar for epic
merge_request: merge_request:
author: author:
type: fixed type: added
---
title: Fix GPG signature popup info in Safari and Firefox
merge_request: 15228
author:
type: fixed
---
title: Fix webhooks recent deliveries
merge_request: 15146
author: Alexander Randa (@randaalex)
type: fixed
---
title: Fix issues with forked projects of which the source was deleted
merge_request: 15150
author:
type: fixed
---
title: Make sure group and project creation is blocked for new users that are external
by default
merge_request:
author:
type: fixed
---
title: Fix arguments Import/Export error importing project merge requests
merge_request:
author:
type: fixed
---
title: Fix diff parser so it tolerates to diff special markers in the content
merge_request:
author:
type: fixed
---
title: Fix a migration that adds merge_requests_ff_only_enabled column to MR table
merge_request:
author:
type: fixed
---
title: Render 404 when polling commit notes without having permissions
merge_request:
author:
type: fixed
---
title: Fix cancel button not working while uploading on the new issue page
merge_request: 15137
author:
type: fixed
---
title: Remove Filesystem check metrics that use too much CPU to handle requests
merge_request:
author:
type: performance
---
title: Avoid regenerating the ref path for the environment
merge_request:
author:
type: fixed
<script> <script>
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
import epicHeader from './epic_header.vue'; import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
export default { export default {
name: 'epicShowApp', name: 'epicShowApp',
...@@ -55,9 +56,18 @@ ...@@ -55,9 +56,18 @@
type: Object, type: Object,
required: true, required: true,
}, },
startDate: {
type: String,
required: false,
},
endDate: {
type: String,
required: false,
},
}, },
components: { components: {
epicHeader, epicHeader,
epicSidebar,
issuableApp, issuableApp,
}, },
created() { created() {
...@@ -75,21 +85,29 @@ ...@@ -75,21 +85,29 @@
:author="author" :author="author"
:created="created" :created="created"
/> />
<div class="issuable-details detail-page-description content-block"> <div class="issuable-details content-block">
<issuable-app <div class="detail-page-description">
:can-update="canUpdate" <issuable-app
:can-destroy="canDestroy" :can-update="canUpdate"
:can-destroy="canDestroy"
:endpoint="endpoint"
:issuable-ref="issuableRef"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
/>
</div>
<epic-sidebar
:endpoint="endpoint" :endpoint="endpoint"
:issuable-ref="issuableRef" :editable="canUpdate"
:initial-title-html="initialTitleHtml" :initialStartDate="startDate"
:initial-title-text="initialTitleText" :initialEndDate="endDate"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
/> />
</div> </div>
</div> </div>
......
...@@ -12,6 +12,10 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -12,6 +12,10 @@ document.addEventListener('DOMContentLoaded', () => {
canDestroy: false, canDestroy: false,
}); });
// Convert backend casing to match frontend style guide
props.startDate = props.start_date;
props.endDate = props.end_date;
return new Vue({ return new Vue({
el, el,
components: { components: {
......
<script>
import Cookies from 'js-cookie';
import Flash from '~/flash';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import sidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import SidebarService from '../services/sidebar_service';
import Store from '../stores/sidebar_store';
export default {
name: 'epicSidebar',
props: {
endpoint: {
type: String,
required: true,
},
editable: {
type: Boolean,
required: false,
default: false,
},
initialStartDate: {
type: String,
required: false,
},
initialEndDate: {
type: String,
required: false,
},
},
data() {
const store = new Store({
startDate: this.initialStartDate,
endDate: this.initialEndDate,
});
return {
store,
// Backend will pass the appropriate css class for the contentContainer
collapsed: Cookies.get('collapsed_gutter') === 'true',
savingStartDate: false,
savingEndDate: false,
service: new SidebarService(this.endpoint),
};
},
components: {
sidebarDatePicker,
sidebarCollapsedGroupedDatePicker,
},
methods: {
toggleSidebar() {
this.collapsed = !this.collapsed;
const contentContainer = this.$el.closest('.page-with-sidebar');
contentContainer.classList.toggle('right-sidebar-expanded');
contentContainer.classList.toggle('right-sidebar-collapsed');
Cookies.set('collapsed_gutter', this.collapsed);
},
saveDate(dateType = 'start', newDate) {
const type = dateType === 'start' ? dateType : 'end';
const capitalizedType = capitalizeFirstCharacter(type);
const serviceMethod = `update${capitalizedType}Date`;
const savingBoolean = `saving${capitalizedType}Date`;
this[savingBoolean] = true;
return this.service[serviceMethod](newDate)
.then(() => {
this[savingBoolean] = false;
this.store[`${type}Date`] = newDate;
})
.catch(() => {
this[savingBoolean] = false;
Flash(`An error occurred while saving ${type} date`);
});
},
saveStartDate(date) {
return this.saveDate('start', date);
},
saveEndDate(date) {
return this.saveDate('end', date);
},
},
};
</script>
<template>
<aside
class="right-sidebar"
:class="{ 'right-sidebar-expanded' : !collapsed, 'right-sidebar-collapsed': collapsed }"
>
<div class="issuable-sidebar">
<sidebar-date-picker
v-if="!collapsed"
:collapsed="collapsed"
:is-loading="savingStartDate"
:editable="editable"
label="Planned start date"
:selected-date="store.startDateTime"
:max-date="store.endDateTime"
:show-toggle-sidebar="true"
@saveDate="saveStartDate"
@toggleCollapse="toggleSidebar"
/>
<sidebar-date-picker
v-if="!collapsed"
:collapsed="collapsed"
:is-loading="savingEndDate"
:editable="editable"
label="Planned finish date"
:selected-date="store.endDateTime"
:min-date="store.startDateTime"
@saveDate="saveEndDate"
@toggleCollapse="toggleSidebar"
/>
<sidebar-collapsed-grouped-date-picker
v-if="collapsed"
:collapsed="collapsed"
:min-date="store.startDateTime"
:max-date="store.endDateTime"
:show-toggle-sidebar="true"
@toggleCollapse="toggleSidebar"
/>
</div>
</aside>
</template>
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class SidebarService {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}.json`, {});
}
updateStartDate(startDate) {
return this.resource.update({
start_date: startDate,
});
}
updateEndDate(endDate) {
return this.resource.update({
end_date: endDate,
});
}
}
import { parsePikadayDate } from '~/lib/utils/datefix';
export default class SidebarStore {
constructor({ startDate, endDate }) {
this.startDate = startDate;
this.endDate = endDate;
}
get startDateTime() {
return this.startDate ? parsePikadayDate(this.startDate) : null;
}
get endDateTime() {
return this.endDate ? parsePikadayDate(this.endDate) : null;
}
}
...@@ -9,7 +9,9 @@ module EpicsHelper ...@@ -9,7 +9,9 @@ module EpicsHelper
url: user_path(author), url: user_path(author),
username: "@#{author.username}", username: "@#{author.username}",
src: avatar_icon(@epic.author) src: avatar_icon(@epic.author)
} },
start_date: @epic.start_date,
end_date: @epic.end_date
} }
data.to_json data.to_json
......
module EE
module MergeRequests
module MergeService
def error_check!
raise NotImplementedError unless defined?(super)
check_size_limit
super
end
def source
return merge_request.diff_head_sha unless merge_request.squash
squash_result = ::MergeRequests::SquashService.new(project, current_user, params).execute(merge_request)
case squash_result[:status]
when :success
squash_result[:squash_sha]
when :error
raise ::MergeRequests::MergeService::MergeError, squash_result[:message]
end
end
private
def check_size_limit
if @merge_request.target_project.above_size_limit?
message = ::Gitlab::RepositorySizeError.new(@merge_request.target_project).merge_error
raise ::MergeRequests::MergeService::MergeError, message
end
end
end
end
end
...@@ -4,4 +4,4 @@ require_relative '../qa' ...@@ -4,4 +4,4 @@ require_relative '../qa'
QA::Scenario QA::Scenario
.const_get(ARGV.shift) .const_get(ARGV.shift)
.launch!(*ARGV) .launch!(ARGV)
...@@ -11,6 +11,8 @@ module QA ...@@ -11,6 +11,8 @@ module QA
module ClassMethods module ClassMethods
def launch!(argv) def launch!(argv)
return self.perform(*argv) unless has_attributes?
arguments = OptionParser.new do |parser| arguments = OptionParser.new do |parser|
options.to_a.each do |opt| options.to_a.each do |opt|
parser.on(opt.arg, opt.desc) do |value| parser.on(opt.arg, opt.desc) do |value|
...@@ -21,11 +23,7 @@ module QA ...@@ -21,11 +23,7 @@ module QA
arguments.parse!(argv) arguments.parse!(argv)
if has_attributes? self.perform(**Runtime::Scenario.attributes)
self.perform(**Runtime::Scenario.attributes)
else
self.perform(*argv)
end
end end
private private
......
#!/bin/bash
mysql --user=root --host=mysql <<EOF
CREATE DATABASE IF NOT EXISTS gitlabhq_test;
CREATE USER IF NOT EXISTS 'gitlab'@'%';
GRANT ALL PRIVILEGES ON gitlabhq_test.* TO 'gitlab'@'%';
FLUSH PRIVILEGES;
EOF
#!/bin/bash
psql -h postgres -U postgres postgres <<EOF
DROP DATABASE IF EXISTS gitlabhq_test;
CREATE DATABASE gitlabhq_test;
CREATE USER gitlab;
GRANT ALL PRIVILEGES ON DATABASE gitlabhq_test TO gitlab;
EOF
. scripts/utils.sh . scripts/utils.sh
export SETUP_DB=${SETUP_DB:-true} export SETUP_DB=${SETUP_DB:-true}
export CREATE_DB_USER=${CREATE_DB_USER:-$SETUP_DB}
export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true} export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet" export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet"
...@@ -29,6 +30,9 @@ cp config/database.yml.$GITLAB_DATABASE config/database.yml ...@@ -29,6 +30,9 @@ cp config/database.yml.$GITLAB_DATABASE config/database.yml
# EE-only # EE-only
cp config/database_geo.yml.$GITLAB_DATABASE config/database_geo.yml cp config/database_geo.yml.$GITLAB_DATABASE config/database_geo.yml
# Set user to a non-superuser to ensure we test permissions
sed -i 's/username: root/username: gitlab/g' config/database.yml
if [ "$GITLAB_DATABASE" = 'postgresql' ]; then if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
sed -i 's/localhost/postgres/g' config/database.yml sed -i 's/localhost/postgres/g' config/database.yml
...@@ -53,6 +57,16 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml ...@@ -53,6 +57,16 @@ sed -i 's/localhost/redis/g' config/redis.queues.yml
cp config/redis.shared_state.yml.example config/redis.shared_state.yml cp config/redis.shared_state.yml.example config/redis.shared_state.yml
sed -i 's/localhost/redis/g' config/redis.shared_state.yml sed -i 's/localhost/redis/g' config/redis.shared_state.yml
# Some tasks (e.g. db:seed_fu) need to have a properly-configured database
# user but not necessarily a full schema loaded
if [ "$CREATE_DB_USER" != "false" ]; then
if [ "$GITLAB_DATABASE" = 'postgresql' ]; then
. scripts/create_postgres_user.sh
else
. scripts/create_mysql_user.sh
fi
fi
if [ "$SETUP_DB" != "false" ]; then if [ "$SETUP_DB" != "false" ]; then
bundle exec rake db:drop db:create db:schema:load db:migrate bundle exec rake db:drop db:create db:schema:load db:migrate
......
...@@ -8,7 +8,7 @@ describe EpicsHelper do ...@@ -8,7 +8,7 @@ describe EpicsHelper do
user = create(:user) user = create(:user)
@epic = create(:epic, author: user) @epic = create(:epic, author: user)
expect(JSON.parse(epic_meta_data).keys).to match_array(%w[created author]) expect(JSON.parse(epic_meta_data).keys).to match_array(%w[created author start_date end_date])
expect(JSON.parse(epic_meta_data)['author']).to eq({ expect(JSON.parse(epic_meta_data)['author']).to eq({
'name' => user.name, 'name' => user.name,
'url' => "/#{user.username}", 'url' => "/#{user.username}",
......
require 'spec_helper'
describe MergeRequests::MergeService do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, :simple) }
let(:project) { merge_request.project }
before do
project.add_master(user)
end
describe '#execute' do
context 'project has exceeded size limit' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
before do
allow(project).to receive(:above_size_limit?).and_return(true)
perform_enqueued_jobs do
service.execute(merge_request)
end
end
it 'returns the correct error message' do
expect(merge_request.merge_error).to include('This merge request cannot be merged')
end
end
end
end
...@@ -67,6 +67,28 @@ feature 'Create New Merge Request', :js do ...@@ -67,6 +67,28 @@ feature 'Create New Merge Request', :js do
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end end
it 'allows filtering multiple dropdowns' do
visit project_new_merge_request_path(project)
first('.js-source-branch').click
input = find('.dropdown-source-branch .dropdown-input-field')
input.click
input.send_keys('orphaned-branch')
find('.dropdown-source-branch .dropdown-content li', match: :first)
source_items = all('.dropdown-source-branch .dropdown-content li')
expect(source_items.count).to eq(1)
first('.js-target-branch').click
find('.dropdown-target-branch .dropdown-content li', match: :first)
target_items = all('.dropdown-target-branch .dropdown-content li')
expect(target_items.count).to be > 1
end
context 'when approvals are disabled for the target project' do context 'when approvals are disabled for the target project' do
it 'does not show approval settings' do it 'does not show approval settings' do
visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'feature_conflict' }) visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'feature_conflict' })
......
import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import * as datetimeUtility from '~/lib/utils/datetime_utility';
(() => { (() => {
describe('Date time utils', () => { describe('Date time utils', () => {
...@@ -89,10 +89,22 @@ import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; ...@@ -89,10 +89,22 @@ import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
describe('timeIntervalInWords', () => { describe('timeIntervalInWords', () => {
it('should return string with number of minutes and seconds', () => { it('should return string with number of minutes and seconds', () => {
expect(timeIntervalInWords(9.54)).toEqual('9 seconds'); expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds');
expect(timeIntervalInWords(1)).toEqual('1 second'); expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second');
expect(timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
expect(timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
});
});
describe('dateInWords', () => {
const date = new Date('07/01/2016');
it('should return date in words', () => {
expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016');
});
it('should return abbreviated month name', () => {
expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016');
}); });
}); });
})(); })();
import Vue from 'vue'; import Vue from 'vue';
import epicShowApp from 'ee/epics/epic_show/components/epic_show_app.vue'; import epicShowApp from 'ee/epics/epic_show/components/epic_show_app.vue';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue'; import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import issuableApp from '~/issue_show/components/app.vue'; import issuableApp from '~/issue_show/components/app.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper'; import mountComponent from '../../../helpers/vue_mount_component_helper';
import { props } from '../mock_data'; import { props } from '../mock_data';
...@@ -10,6 +11,7 @@ describe('EpicShowApp', () => { ...@@ -10,6 +11,7 @@ describe('EpicShowApp', () => {
let vm; let vm;
let headerVm; let headerVm;
let issuableAppVm; let issuableAppVm;
let sidebarVm;
const interceptor = (request, next) => { const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(issueShowData.initialRequest), { next(request.respondWith(JSON.stringify(issueShowData.initialRequest), {
...@@ -26,6 +28,8 @@ describe('EpicShowApp', () => { ...@@ -26,6 +28,8 @@ describe('EpicShowApp', () => {
endpoint, endpoint,
initialTitleHtml, initialTitleHtml,
initialTitleText, initialTitleText,
startDate,
endDate,
markdownPreviewPath, markdownPreviewPath,
markdownDocsPath, markdownDocsPath,
author, author,
...@@ -57,6 +61,14 @@ describe('EpicShowApp', () => { ...@@ -57,6 +61,14 @@ describe('EpicShowApp', () => {
projectNamespace: '', projectNamespace: '',
showInlineEditButton: true, showInlineEditButton: true,
}); });
const EpicSidebar = Vue.extend(epicSidebar);
sidebarVm = mountComponent(EpicSidebar, {
endpoint,
editable: canUpdate,
initialStartDate: startDate,
initialEndDate: endDate,
});
}); });
afterEach(() => { afterEach(() => {
...@@ -70,4 +82,8 @@ describe('EpicShowApp', () => { ...@@ -70,4 +82,8 @@ describe('EpicShowApp', () => {
it('should render issuable-app', () => { it('should render issuable-app', () => {
expect(vm.$el.innerHTML.indexOf(issuableAppVm.$el.innerHTML) !== -1).toEqual(true); expect(vm.$el.innerHTML.indexOf(issuableAppVm.$el.innerHTML) !== -1).toEqual(true);
}); });
it('should render epic-sidebar', () => {
expect(vm.$el.innerHTML.indexOf(sidebarVm.$el.innerHTML) !== -1).toEqual(true);
});
}); });
...@@ -7,6 +7,8 @@ export const contentProps = { ...@@ -7,6 +7,8 @@ export const contentProps = {
groupPath: '', groupPath: '',
initialTitleHtml: '', initialTitleHtml: '',
initialTitleText: '', initialTitleText: '',
startDate: '2017-01-01',
endDate: '2017-10-10',
}; };
export const headerProps = { export const headerProps = {
......
import SidebarStore from 'ee/epics/sidebar/stores/sidebar_store';
describe('Sidebar Store', () => {
const dateString = '2017-01-20';
describe('constructor', () => {
it('should set startDate', () => {
const store = new SidebarStore({
startDate: dateString,
});
expect(store.startDate).toEqual(dateString);
});
it('should set endDate', () => {
const store = new SidebarStore({
endDate: dateString,
});
expect(store.endDate).toEqual(dateString);
});
});
describe('startDateTime', () => {
it('should return null when there is no startDate', () => {
const store = new SidebarStore({});
expect(store.startDateTime).toEqual(null);
});
it('should return date', () => {
const store = new SidebarStore({
startDate: dateString,
});
const date = store.startDateTime;
expect(date.getDate()).toEqual(20);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2017);
});
});
describe('endDateTime', () => {
it('should return null when there is no endDate', () => {
const store = new SidebarStore({});
expect(store.endDateTime).toEqual(null);
});
it('should return date', () => {
const store = new SidebarStore({
endDate: dateString,
});
const date = store.endDateTime;
expect(date.getDate()).toEqual(20);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2017);
});
});
});
import { highCountTrim } from '~/lib/utils/text_utility'; import * as textUtility from '~/lib/utils/text_utility';
describe('text_utility', () => { describe('text_utility', () => {
describe('gl.text.getTextWidth', () => { describe('gl.text.getTextWidth', () => {
...@@ -37,12 +37,18 @@ describe('text_utility', () => { ...@@ -37,12 +37,18 @@ describe('text_utility', () => {
describe('highCountTrim', () => { describe('highCountTrim', () => {
it('returns 99+ for count >= 100', () => { it('returns 99+ for count >= 100', () => {
expect(highCountTrim(105)).toBe('99+'); expect(textUtility.highCountTrim(105)).toBe('99+');
expect(highCountTrim(100)).toBe('99+'); expect(textUtility.highCountTrim(100)).toBe('99+');
}); });
it('returns exact number for count < 100', () => { it('returns exact number for count < 100', () => {
expect(highCountTrim(45)).toBe(45); expect(textUtility.highCountTrim(45)).toBe(45);
});
});
describe('capitalizeFirstCharacter', () => {
it('returns string with first letter capitalized', () => {
expect(textUtility.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
}); });
}); });
......
import Vue from 'vue';
import datePicker from '~/vue_shared/components/pikaday.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('datePicker', () => {
let vm;
beforeEach(() => {
const DatePicker = Vue.extend(datePicker);
vm = mountComponent(DatePicker, {
label: 'label',
});
});
it('should render label text', () => {
expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label');
});
it('should show calendar', () => {
expect(vm.$el.querySelector('.pika-single')).toBeDefined();
});
it('should toggle when dropdown is clicked', () => {
const hidePicker = jasmine.createSpy();
vm.$on('hidePicker', hidePicker);
vm.$el.querySelector('.dropdown-menu-toggle').click();
expect(hidePicker).toHaveBeenCalled();
});
});
import Vue from 'vue';
import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('collapsedCalendarIcon', () => {
let vm;
beforeEach(() => {
const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon);
vm = mountComponent(CollapsedCalendarIcon, {
containerClass: 'test-class',
text: 'text',
showIcon: false,
});
});
it('should add class to container', () => {
expect(vm.$el.classList.contains('test-class')).toEqual(true);
});
it('should hide calendar icon if showIcon', () => {
expect(vm.$el.querySelector('.fa-calendar')).toBeNull();
});
it('should render text', () => {
expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text');
});
it('should emit click event when container is clicked', () => {
const click = jasmine.createSpy();
vm.$on('click', click);
vm.$el.click();
expect(click).toHaveBeenCalled();
});
});
import Vue from 'vue';
import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('collapsedGroupedDatePicker', () => {
let vm;
beforeEach(() => {
const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker);
vm = mountComponent(CollapsedGroupedDatePicker, {
showToggleSidebar: true,
});
});
it('should render toggle sidebar if showToggleSidebar', (done) => {
expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined();
vm.showToggleSidebar = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull();
done();
});
});
it('toggleCollapse events', () => {
const toggleCollapse = jasmine.createSpy();
beforeEach((done) => {
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
it('should emit when sidebar is toggled', () => {
vm.$el.querySelector('.gutter-toggle').click();
expect(toggleCollapse).toHaveBeenCalled();
});
it('should emit when collapsed-calendar-icon is clicked', () => {
vm.$el.querySelector('.sidebar-collapsed-icon').click();
expect(toggleCollapse).toHaveBeenCalled();
});
});
describe('minDate and maxDate', () => {
beforeEach((done) => {
vm.minDate = new Date('07/17/2016');
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
});
it('should render both collapsed-calendar-icon', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons.length).toEqual(2);
expect(icons[0].innerText.trim()).toEqual('Jul 17 2016');
expect(icons[1].innerText.trim()).toEqual('Jul 17 2017');
});
});
describe('minDate', () => {
beforeEach((done) => {
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
it('should render minDate in collapsed-calendar-icon', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016');
});
});
describe('maxDate', () => {
beforeEach((done) => {
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
});
it('should render maxDate in collapsed-calendar-icon', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017');
});
});
describe('no dates', () => {
it('should render None', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('None');
});
});
});
import Vue from 'vue';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('sidebarDatePicker', () => {
let vm;
beforeEach(() => {
const SidebarDatePicker = Vue.extend(sidebarDatePicker);
vm = mountComponent(SidebarDatePicker, {
label: 'label',
isLoading: true,
});
});
it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
const toggleCollapse = jasmine.createSpy();
vm.$on('toggleCollapse', toggleCollapse);
vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click();
expect(toggleCollapse).toHaveBeenCalled();
});
it('should render collapsed-calendar-icon', () => {
expect(vm.$el.querySelector('.sidebar-collapsed-icon')).toBeDefined();
});
it('should render label', () => {
expect(vm.$el.querySelector('.title').innerText.trim()).toEqual('label');
});
it('should render loading-icon when isLoading', () => {
expect(vm.$el.querySelector('.fa-spin')).toBeDefined();
});
it('should render value when not editing', () => {
expect(vm.$el.querySelector('.value-content')).toBeDefined();
});
it('should render None if there is no selectedDate', () => {
expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None');
});
it('should render date-picker when editing', (done) => {
vm.editing = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.pika-label')).toBeDefined();
done();
});
});
describe('editable', () => {
beforeEach((done) => {
vm.editable = true;
Vue.nextTick(done);
});
it('should render edit button', () => {
expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit');
});
it('should enable editing when edit button is clicked', (done) => {
vm.isLoading = false;
Vue.nextTick(() => {
vm.$el.querySelector('.title .btn-blank').click();
expect(vm.editing).toEqual(true);
done();
});
});
});
it('should render date if selectedDate', (done) => {
vm.selectedDate = new Date('07/07/2017');
Vue.nextTick(() => {
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017');
done();
});
});
describe('selectedDate and editable', () => {
beforeEach((done) => {
vm.selectedDate = new Date('07/07/2017');
vm.editable = true;
Vue.nextTick(done);
});
it('should render remove button if selectedDate and editable', () => {
expect(vm.$el.querySelector('.value-content .btn-blank').innerText.trim()).toEqual('remove');
});
it('should emit saveDate when remove button is clicked', () => {
const saveDate = jasmine.createSpy();
vm.$on('saveDate', saveDate);
vm.$el.querySelector('.value-content .btn-blank').click();
expect(saveDate).toHaveBeenCalled();
});
});
describe('showToggleSidebar', () => {
beforeEach((done) => {
vm.showToggleSidebar = true;
Vue.nextTick(done);
});
it('should render toggle-sidebar when showToggleSidebar', () => {
expect(vm.$el.querySelector('.title .gutter-toggle')).toBeDefined();
});
it('should emit toggleCollapse when toggle sidebar is clicked', () => {
const toggleCollapse = jasmine.createSpy();
vm.$on('toggleCollapse', toggleCollapse);
vm.$el.querySelector('.title .gutter-toggle').click();
expect(toggleCollapse).toHaveBeenCalled();
});
});
});
import Vue from 'vue';
import Cookies from 'js-cookie';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('epicSidebar', () => {
let vm;
let originalCookieState;
let EpicSidebar;
beforeEach(() => {
setFixtures(`
<div class="page-with-sidebar right-sidebar-expanded">
<div id="epic-sidebar"></div>
</div>
`);
originalCookieState = Cookies.get('collapsed_gutter');
Cookies.set('collapsed_gutter', null);
EpicSidebar = Vue.extend(epicSidebar);
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
}, '#epic-sidebar');
});
afterEach(() => {
Cookies.set('collapsed_gutter', originalCookieState);
});
it('should render right-sidebar-expanded class when not collapsed', () => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toEqual(true);
});
it('should render min date sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
});
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017');
});
it('should render max date sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialEndDate: '2018-01-01',
});
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018');
});
it('should render both sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
initialEndDate: '2018-01-01',
});
const datePickers = vm.$el.querySelectorAll('.block');
expect(datePickers[0].querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017');
expect(datePickers[1].querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018');
});
describe('when collapsed', () => {
beforeEach(() => {
Cookies.set('collapsed_gutter', 'true');
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
});
});
it('should render right-sidebar-collapsed class', () => {
expect(vm.$el.classList.contains('right-sidebar-collapsed')).toEqual(true);
});
it('should render collapsed grouped date picker', () => {
expect(vm.$el.querySelector('.sidebar-collapsed-icon span').innerText.trim()).toEqual('From Jan 1 2017');
});
});
describe('toggleSidebar', () => {
it('should toggle collapsed_gutter cookie', () => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toEqual(true);
vm.$el.querySelector('.gutter-toggle').click();
expect(Cookies.get('collapsed_gutter')).toEqual('true');
});
it('should toggle contentContainer css class', () => {
const contentContainer = document.querySelector('.page-with-sidebar');
expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(true);
expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(false);
vm.$el.querySelector('.gutter-toggle').click();
expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(false);
expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(true);
});
});
describe('saveDate', () => {
let interceptor;
let component;
beforeEach(() => {
interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 200,
}));
};
Vue.http.interceptors.push(interceptor);
component = new EpicSidebar({
propsData: {
endpoint: gl.TEST_HOST,
},
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should save startDate', (done) => {
const date = '2017-01-01';
expect(component.store.startDate).toBeUndefined();
component.saveStartDate(date)
.then(() => {
expect(component.store.startDate).toEqual(date);
done();
})
.catch(done.fail);
});
it('should save endDate', (done) => {
const date = '2017-01-01';
expect(component.store.endDate).toBeUndefined();
component.saveEndDate(date)
.then(() => {
expect(component.store.endDate).toEqual(date);
done();
})
.catch(done.fail);
});
it('should handle errors gracefully', () => {});
});
describe('saveDate error', () => {
let interceptor;
let component;
beforeEach(() => {
interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 500,
}));
};
Vue.http.interceptors.push(interceptor);
component = new EpicSidebar({
propsData: {
endpoint: gl.TEST_HOST,
},
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should handle errors gracefully', (done) => {
const date = '2017-01-01';
expect(component.store.startDate).toBeUndefined();
component.saveDate('start', date)
.then(() => {
expect(component.store.startDate).toBeUndefined();
done();
})
.catch(done.fail);
});
});
});
import Vue from 'vue';
import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('toggleSidebar', () => {
let vm;
beforeEach(() => {
const ToggleSidebar = Vue.extend(toggleSidebar);
vm = mountComponent(ToggleSidebar, {
collapsed: true,
});
});
it('should render << when collapsed', () => {
expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true);
});
it('should render >> when collapsed', () => {
vm.collapsed = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true);
});
});
it('should emit toggle event when button clicked', () => {
const toggle = jasmine.createSpy();
vm.$on('toggle', toggle);
vm.$el.click();
expect(toggle).toHaveBeenCalled();
});
});
...@@ -38,22 +38,6 @@ describe MergeRequests::MergeService do ...@@ -38,22 +38,6 @@ describe MergeRequests::MergeService do
end end
end end
context 'project has exceeded size limit' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
before do
allow(project).to receive(:above_size_limit?).and_return(true)
perform_enqueued_jobs do
service.execute(merge_request)
end
end
it 'returns the correct error message' do
expect(merge_request.merge_error).to include('This merge request cannot be merged')
end
end
context 'closes related issues' do context 'closes related issues' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
...@@ -297,6 +281,28 @@ describe MergeRequests::MergeService do ...@@ -297,6 +281,28 @@ describe MergeRequests::MergeService do
expect(merge_request.merge_error).to include(error_message) expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end end
context "when fast-forward merge is not allowed" do
before do
allow_any_instance_of(Repository).to receive(:ancestor?).and_return(nil)
end
%w(semi-linear ff).each do |merge_method|
it "logs and saves error if merge is #{merge_method} only" do
merge_method = 'rebase_merge' if merge_method == 'semi-linear'
merge_request.project.update(merge_method: merge_method)
error_message = 'Only fast-forward merge is allowed for your project. Please update your source branch'
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
expect(merge_request).to be_open
expect(merge_request.merge_commit_sha).to be_nil
expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
end
end
end end
end end
end end
......
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