Commit 23f5d3c1 authored by Stan Hu's avatar Stan Hu

Merge branch 'ce-to-ee-2018-06-15' into 'master'

CE upstream - 2018-06-15 18:21 UTC

See merge request gitlab-org/gitlab-ee!6156
parents 74c10876 603a6be9
......@@ -77,3 +77,4 @@ eslint-report.html
/.rspec
/plugins/*
/.gitlab_pages_secret
package-lock.json
......@@ -123,7 +123,7 @@ gl.issueBoards.BoardsStore = {
if (!issueTo) {
// Check if target list assignee is already present in this issue
if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
issue.findAssignee(listTo.assignee)) {
issue.findAssignee(listTo.assignee)) {
const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee);
} else {
......@@ -144,7 +144,7 @@ gl.issueBoards.BoardsStore = {
issue.removeAssignee(listFrom.assignee);
listFrom.removeIssue(issue);
} else if ((listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label')) {
(listTo.type !== 'assignee' && listFrom.type === 'label')) {
listFrom.removeIssue(issue);
}
},
......@@ -162,13 +162,9 @@ gl.issueBoards.BoardsStore = {
});
return filteredList[0];
},
updateFiltersUrl (replaceState = false) {
if (replaceState) {
window.history.replaceState(null, null, `?${this.filter.path}`);
} else {
window.history.pushState(null, null, `?${this.filter.path}`);
}
},
updateFiltersUrl () {
window.history.pushState(null, null, `?${this.filter.path}`);
}
};
boardsStoreEE.initEESpecific(gl.issueBoards.BoardsStore);
......@@ -34,6 +34,10 @@ export default {
type: String,
required: true,
},
actionBtnIcon: {
type: String,
required: true,
},
itemActionComponent: {
type: String,
required: true,
......@@ -53,26 +57,21 @@ export default {
required: true,
},
},
data() {
return {
showActionButton: false,
};
},
computed: {
titleText() {
return sprintf(__('%{title} changes'), {
title: this.title,
});
},
filesLength() {
return this.fileList.length;
},
},
methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() {
this[this.action]();
},
setShowActionButton(show) {
this.showActionButton = show;
},
},
};
</script>
......@@ -83,8 +82,6 @@ export default {
>
<header
class="multi-file-commit-panel-header"
@mouseenter="setShowActionButton(true)"
@mouseleave="setShowActionButton(false)"
>
<div
class="multi-file-commit-panel-header-title"
......@@ -95,24 +92,40 @@ export default {
:size="18"
/>
{{ titleText }}
<span
v-show="!showActionButton"
class="ide-commit-file-count"
>
{{ fileList.length }}
</span>
<button
v-show="showActionButton"
type="button"
class="btn btn-blank btn-link ide-staged-action-btn"
@click="actionBtnClicked"
>
{{ actionBtnText }}
</button>
<div class="d-flex ml-auto">
<button
v-tooltip
v-show="filesLength"
:class="{
'd-flex': filesLength
}"
:title="actionBtnText"
type="button"
class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center"
data-placement="bottom"
data-container="body"
data-boundary="viewport"
@click="actionBtnClicked"
>
<icon
:name="actionBtnIcon"
:size="12"
class="ml-auto mr-auto"
/>
</button>
<span
:class="{
'rounded-right': !filesLength
}"
class="ide-commit-file-count order-0 rounded-left text-center"
>
{{ filesLength }}
</span>
</div>
</div>
</header>
<ul
v-if="fileList.length"
v-if="filesLength"
class="multi-file-commit-list list-unstyled append-bottom-0"
>
<li
......
<script>
import { mapActions } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
......@@ -11,6 +12,9 @@ export default {
StageButton,
UnstageButton,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
......@@ -50,6 +54,9 @@ export default {
isActive() {
return this.activeFileKey === this.fullKey;
},
tooltipTitle() {
return this.file.path === this.file.name ? '' : this.file.path;
},
},
methods: {
...mapActions([
......@@ -81,29 +88,30 @@ export default {
</script>
<template>
<div
:class="{
'is-active': isActive
}"
class="multi-file-commit-list-item"
>
<div class="multi-file-commit-list-item position-relative">
<button
v-tooltip
:title="tooltipTitle"
:class="{
'is-active': isActive
}"
type="button"
class="multi-file-commit-list-path"
class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
@dblclick="fileAction"
@click="openFileInEditor"
>
<span class="multi-file-commit-list-file-path">
<span class="multi-file-commit-list-file-path d-flex align-items-center">
<icon
:name="iconName"
:size="16"
:css-classes="iconClass"
/>{{ file.path }}
/>{{ file.name }}
</span>
</button>
<component
:is="actionComponent"
:path="file.path"
class="d-flex position-absolute"
/>
</div>
</template>
......@@ -25,15 +25,17 @@ export default {
<template>
<div
v-once
class="multi-file-discard-btn"
class="multi-file-discard-btn dropdown"
>
<button
v-tooltip
:aria-label="__('Stage changes')"
:title="__('Stage changes')"
type="button"
class="btn btn-blank append-right-5"
class="btn btn-blank append-right-5 d-flex align-items-center"
data-container="body"
data-boundary="viewport"
data-placement="bottom"
@click.stop="stageChange(path)"
>
<icon
......@@ -43,17 +45,31 @@ export default {
</button>
<button
v-tooltip
:aria-label="__('Discard changes')"
:title="__('Discard changes')"
:title="__('More actions')"
type="button"
class="btn btn-blank"
class="btn btn-blank d-flex align-items-center"
data-container="body"
@click.stop="discardFileChanges(path)"
data-boundary="viewport"
data-placement="bottom"
data-toggle="dropdown"
data-display="static"
>
<icon
:size="12"
name="remove"
name="more"
/>
</button>
<div class="dropdown-menu dropdown-menu-right">
<ul>
<li>
<button
type="button"
@click.stop="discardFileChanges(path)"
>
{{ __('Discard changes') }}
</button>
</li>
</ul>
</div>
</div>
</template>
......@@ -32,8 +32,10 @@ export default {
:aria-label="__('Unstage changes')"
:title="__('Unstage changes')"
type="button"
class="btn btn-blank"
class="btn btn-blank d-flex align-items-center"
data-container="body"
data-boundary="viewport"
data-placement="bottom"
@click="unstageChange(path)"
>
<icon
......
......@@ -93,23 +93,25 @@ export default {
:title="__('Unstaged')"
:key-prefix="$options.stageKeys.unstaged"
:file-list="changedFiles"
:action-btn-text="__('Stage all')"
:action-btn-text="__('Stage all changes')"
:active-file-key="activeFileKey"
class="is-first"
icon-name="unstaged"
action="stageAllChanges"
action-btn-icon="mobile-issue-close"
item-action-component="stage-button"
class="is-first"
icon-name="unstaged"
/>
<commit-files-list
:title="__('Staged')"
:key-prefix="$options.stageKeys.staged"
:file-list="stagedFiles"
:action-btn-text="__('Unstage all')"
:action-btn-text="__('Unstage all changes')"
:staged-list="true"
:active-file-key="activeFileKey"
icon-name="staged"
action="unstageAllChanges"
action-btn-icon="history"
item-action-component="unstage-button"
icon-name="staged"
/>
</template>
<empty-state
......
......@@ -870,3 +870,4 @@ $font-family-sans-serif: $regular_font;
$font-family-monospace: $monospace_font;
$input-line-height: 20px;
$btn-line-height: 20px;
$table-accent-bg: $gray-light;
......@@ -280,7 +280,7 @@
width: 150px;
flex-shrink: 0;
.label {
.badge {
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
......
......@@ -540,36 +540,12 @@
margin-right: -$grid-size;
min-height: 60px;
.multi-file-commit-list-item {
margin-left: 0;
margin-right: 0;
}
&.form-text.text-muted {
margin-left: 0;
right: 0;
}
}
.multi-file-commit-list-item {
&.is-active {
background-color: $white-normal;
}
.multi-file-discard-btn {
display: none;
margin-top: -2px;
margin-left: auto;
color: $gl-link-color;
}
&:hover {
.multi-file-discard-btn {
display: flex;
}
}
}
.multi-file-addition,
.multi-file-addition-solid {
color: $green-500;
......@@ -599,7 +575,7 @@
}
}
.multi-file-commit-list-item,
.multi-file-commit-list-path,
.ide-file-list .file {
display: flex;
align-items: center;
......@@ -616,11 +592,9 @@
}
.multi-file-commit-list-path {
padding: 0;
background: none;
border: 0;
text-align: left;
width: 100%;
&.is-active {
background-color: $white-normal;
}
&:hover,
&:focus {
......@@ -635,7 +609,7 @@
}
.multi-file-commit-list-file-path {
@include str-truncated(100%);
@include str-truncated(calc(100% - 30px));
&:hover {
text-decoration: underline;
......@@ -646,6 +620,16 @@
}
}
.multi-file-discard-btn {
top: 4px;
right: 8px;
bottom: 4px;
svg {
top: 0;
}
}
.multi-file-commit-form {
position: relative;
background-color: $white-light;
......@@ -840,18 +824,20 @@
}
.ide-staged-action-btn {
margin-left: auto;
line-height: 22px;
width: 22px;
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
> svg {
top: 0;
}
}
.ide-commit-file-count {
min-width: 22px;
margin-left: auto;
background-color: $gray-light;
border-radius: $border-radius-default;
border: 1px solid $white-dark;
line-height: 20px;
text-align: center;
}
.ide-commit-radios {
......
......@@ -123,9 +123,9 @@ class Projects::MilestonesController < Projects::ApplicationController
def search_params
if request.format.json? && @project.group && can?(current_user, :read_group, @project.group)
groups = @project.group.self_and_ancestors
groups = @project.group.self_and_ancestors_ids
end
params.permit(:state).merge(project_ids: @project.id, group_ids: groups&.select(:id))
params.permit(:state).merge(project_ids: @project.id, group_ids: groups)
end
end
module Resolvers
class MergeRequestResolver < BaseResolver
prepend FullPathResolver
type Types::ProjectType, null: true
argument :iid, GraphQL::ID_TYPE,
required: true,
description: 'The IID of the merge request, e.g., "1"'
def resolve(full_path:, iid:)
project = model_by_full_path(Project, full_path)
type Types::MergeRequestType, null: true
alias_method :project, :object
def resolve(iid:)
return unless project.present?
BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader|
......
......@@ -61,5 +61,12 @@ module Types
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true
field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :merge_request,
Types::MergeRequestType,
null: true,
resolver: Resolvers::MergeRequestResolver do
authorize :read_merge_request
end
end
end
......@@ -9,13 +9,6 @@ module Types
authorize :read_project
end
field :merge_request, Types::MergeRequestType,
null: true,
resolver: Resolvers::MergeRequestResolver,
description: "Find a merge request" do
authorize :read_merge_request
end
field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
end
end
class FaviconUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[png ico].freeze
include CarrierWave::MiniMagick
version :favicon_main do
process resize_to_fill: [32, 32]
process convert: 'png'
def full_filename(filename)
filename_for_different_format(super(filename), 'png')
end
end
def extension_whitelist
EXTENSION_WHITELIST
end
......
......@@ -25,7 +25,7 @@
= f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label'
.col-sm-10
- if @appearance.favicon?
= image_tag @appearance.favicon.favicon_main.url, class: 'appearance-light-logo-preview'
= image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
= link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo"
......@@ -33,9 +33,9 @@
= f.hidden_field :favicon_cache
= f.file_field :favicon, class: ''
.hint
Maximum file size is 1MB. Allowed image formats are #{favicon_extension_whitelist}.
Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}.
%br
The resulting favicons will be cropped to be square and scaled down to a size of 32x32 px.
Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior.
= render partial: 'admin/appearances/system_header_footer_form', locals: { form: f }
......
......@@ -9,7 +9,7 @@
None
%a{ href: "#",
"v-for" => "label in issue.labels" }
%span.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
{{ label.title }}
- if can_admin_issue?
.selectbox
......
---
title: "[Rails5] Fix optimistic lock value"
merge_request: 19878
author: "@blackst0ne"
type: fixed
---
title: Allow querying a single merge request within a project
merge_request: 19853
author:
type: changed
---
title: Improve Web IDE commit flow
merge_request:
author:
type: changed
---
title: Rails5 fix passing Group objects array into for_projects_and_groups milestone
scope
merge_request: 19863
author: Jasper Maes
type: fixed
# rubocop:disable Lint/RescueException
# Remove this entire initializer when we are at rails 5.0.
# This file fixes the bug (see below) which has been fixed in the upstream.
unless Gitlab.rails5?
# This patch fixes https://github.com/rails/rails/issues/26024
# TODO: Remove it when it's no longer necessary
module ActiveRecord
module Locking
module Optimistic
# We overwrite this method because we don't want to have default value
# for newly created records
def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
super
end
# Remove this monkey-patch when all lock_version values are converted from NULLs to zeros.
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/25228
module ActiveRecord
module Locking
module Optimistic
# We overwrite this method because we don't want to have default value
# for newly created records
def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
super
end
def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
lock_col = self.class.locking_column
lock_col = self.class.locking_column
previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
# This line is added as a patch
previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
# This line is added as a patch
previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
increment_lock
increment_lock
attribute_names += [lock_col]
attribute_names.uniq!
attribute_names += [lock_col]
attribute_names.uniq!
begin
relation = self.class.unscoped
begin
relation = self.class.unscoped
affected_rows = relation.where(
self.class.primary_key => id,
lock_col => previous_lock_value
).update_all(
attributes_for_update(attribute_names).map do |name|
[name, _read_attribute(name)]
end.to_h
)
affected_rows = relation.where(
self.class.primary_key => id,
lock_col => previous_lock_value
).update_all(
attributes_for_update(attribute_names).map do |name|
[name, _read_attribute(name)]
end.to_h
)
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
end
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
end
affected_rows
affected_rows
# If something went wrong, revert the version.
rescue Exception
send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
raise
end
# If something went wrong, revert the version.
rescue Exception
send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
raise
end
end
# This is patched because we need it to query `lock_version IS NULL`
# rather than `lock_version = 0` whenever lock_version is NULL.
def relation_for_destroy
return super unless locking_enabled?
# This is patched because we need it to query `lock_version IS NULL`
# rather than `lock_version = 0` whenever lock_version is NULL.
def relation_for_destroy
return super unless locking_enabled?
column_name = self.class.locking_column
super.where(self.class.arel_table[column_name].eq(self[column_name]))
end
column_name = self.class.locking_column
super.where(self.class.arel_table[column_name].eq(self[column_name]))
end
end
# This is patched because we want `lock_version` default to `NULL`
# rather than `0`
if Gitlab.rails5?
class LockingType
def deserialize(value)
super
end
# This is patched because we want `lock_version` default to `NULL`
# rather than `0`
def serialize(value)
super
end
end
else
class LockingType < SimpleDelegator
def type_cast_from_database(value)
super
......
......@@ -29,9 +29,7 @@ curl --data "value=100" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://g
## Available queries
A first iteration of a GraphQL API includes only 2 queries: `project` and
`merge_request` and only returns scalar fields, or fields of the type `Project`
or `MergeRequest`.
A first iteration of a GraphQL API includes a query for: `project`. Within a project it is also possible to fetch a `mergeRequest` by IID.
## GraphiQL
......
......@@ -43,3 +43,17 @@ yarn prettier-all-save
Formats all files in the repository with Prettier. (This should only be used to test global rule updates otherwise you would end up with huge MR's).
The source of these Yarn scripts can be found in `/scripts/frontend/prettier.js`.
### Scripts during Conversion period
```
node ./scripts/frontend/prettier.js check ./vendor/
```
This will go over all files in a specific folder check it.
```
node ./scripts/frontend/prettier.js save ./vendor/
```
This will go over all files in a specific folder and save it.
......@@ -35,7 +35,12 @@ In Google's side:
1. You should now be able to see a Client ID and Client secret. Note them down
or keep this page open as you will need them later.
1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google Kubernetes Engine API > Enable**
1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google+ API > Enable**
1. To enable projects to access [Google Kubernetes Engine](../user/project/clusters/index.md), you must also
enable these APIs:
- Google Kubernetes Engine API
- Cloud Resource Manager API
- Cloud Billing API
On your GitLab server:
......
......@@ -28,15 +28,27 @@ class BoardsStoreEE {
this.initBoardFilters();
}
};
this.store.updateFiltersUrl = (replaceState = false) => {
if (replaceState) {
window.history.replaceState(null, null, `?${this.store.filter.path}`);
} else {
window.history.pushState(null, null, `?${this.store.filter.path}`);
}
};
}
initBoardFilters() {
const updateFilterPath = (key, value) => {
if (!value) return;
const querystring = `${key}=${value}`;
this.store.filter.path = [querystring].concat(
this.store.filter.path.split('&').filter(param => param.match(new RegExp(`^${key}=(.*)$`, 'g')) === null),
).join('&');
this.store.filter.path = [querystring]
.concat(
this.store.filter.path
.split('&')
.filter(param => param.match(new RegExp(`^${key}=(.*)$`, 'g')) === null),
)
.join('&');
};
let milestoneTitle = this.store.boardConfig.milestoneTitle;
......@@ -64,7 +76,7 @@ class BoardsStoreEE {
}
const filterPath = this.store.filter.path.split('&');
this.store.boardConfig.labels.forEach((label) => {
this.store.boardConfig.labels.forEach(label => {
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
......@@ -85,7 +97,12 @@ class BoardsStoreEE {
}
addPromotion() {
if (!this.$boardApp.hasAttribute('data-show-promotion') || this.promotionIsHidden() || this.store.disabled) return;
if (
!this.$boardApp.hasAttribute('data-show-promotion') ||
this.promotionIsHidden() ||
this.store.disabled
)
return;
this.store.addList({
id: 'promotion',
......
......@@ -65,7 +65,7 @@ module Banzai
# We don't support IID lookups for group milestones, because IIDs can
# clash between group and project milestones.
if project.group && !params[:iid]
finder_params[:group_ids] = project.group.self_and_ancestors.select(:id)
finder_params[:group_ids] = project.group.self_and_ancestors_ids
end
MilestonesFinder.new(finder_params).find_by(params)
......
......@@ -2,7 +2,7 @@ module Gitlab
class Favicon
class << self
def main
return appearance_favicon.favicon_main.url if appearance_favicon.exists?
return appearance_favicon.url if appearance_favicon.exists?
image_name =
if Gitlab::Utils.to_boolean(ENV['CANARY'])
......
......@@ -9,6 +9,8 @@ const getStagedFiles = require('./frontend_script_utils').getStagedFiles;
const mode = process.argv[2] || 'check';
const shouldSave = mode === 'save' || mode === 'save-all';
const allFiles = mode === 'check-all' || mode === 'save-all';
let dirPath = process.argv[3] || '';
if (dirPath && dirPath.charAt(dirPath.length - 1) !== '/') dirPath += '/';
const config = {
patterns: ['**/*.js', '**/*.vue', '**/*.scss'],
......@@ -39,9 +41,10 @@ prettierIgnore.add(
const availableExtensions = Object.keys(config.parsers);
console.log(`Loading ${allFiles ? 'All' : 'Staged'} Files ...`);
console.log(`Loading ${allFiles ? 'All' : 'Selected'} Files ...`);
const stagedFiles = allFiles ? null : getStagedFiles(availableExtensions.map(ext => `*.${ext}`));
const stagedFiles =
allFiles || dirPath ? null : getStagedFiles(availableExtensions.map(ext => `*.${ext}`));
if (stagedFiles) {
if (!stagedFiles.length || (stagedFiles.length === 1 && !stagedFiles[0])) {
......@@ -60,6 +63,13 @@ if (allFiles) {
const patterns = config.patterns;
const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`;
files = glob.sync(globPattern, { ignore }).filter(f => allFiles || stagedFiles.includes(f));
} else if (dirPath) {
const ignore = config.ignore;
const patterns = config.patterns.map(item => {
return dirPath + item;
});
const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`;
files = glob.sync(globPattern, { ignore });
} else {
files = stagedFiles.filter(f => availableExtensions.includes(f.split('.').pop()));
}
......@@ -73,12 +83,11 @@ if (!files.length) {
console.log(`${shouldSave ? 'Updating' : 'Checking'} ${files.length} file(s)`);
prettier
.resolveConfig('.')
.then(options => {
console.log('Found options : ', options);
files.forEach(file => {
try {
files.forEach(file => {
try {
prettier
.resolveConfig(file)
.then(options => {
const fileExtension = file.split('.').pop();
Object.assign(options, {
parser: config.parsers[fileExtension],
......@@ -101,17 +110,17 @@ prettier
}
console.log(`Prettify Manually : ${file}`);
}
} catch (error) {
didError = true;
console.log(`\n\nError with ${file}: ${error.message}`);
}
});
if (didWarn || didError) {
process.exit(1);
}
})
.catch(e => {
console.log(`Error on loading the Config File: ${e.message}`);
process.exit(1);
});
})
.catch(e => {
console.log(`Error on loading the Config File: ${e.message}`);
process.exit(1);
});
} catch (error) {
didError = true;
console.log(`\n\nError with ${file}: ${error.message}`);
}
});
if (didWarn || didError) {
process.exit(1);
}
......@@ -580,23 +580,6 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(404)
end
end
context 'has a valid filename on the version file' do
it 'successfully returns the file' do
get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'favicon_main_dk.png'
expect(response).to have_gitlab_http_status(200)
expect(response.header['Content-Disposition']).to end_with 'filename="favicon_main_dk.png"'
end
end
context 'has an invalid filename on the version file' do
it 'returns a 404' do
get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'favicon_bogusversion_dk.png'
expect(response).to have_gitlab_http_status(404)
end
end
end
end
end
......@@ -34,7 +34,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
expect(page).to have_selector('span.badge', text: label.title)
expect(page).to have_selector('.badge', text: label.title)
end
end
......@@ -45,7 +45,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests
expect(page).not_to have_selector('span.badge', text: child_group_label.title)
expect(page).not_to have_selector('.badge', text: child_group_label.title)
end
end
......
......@@ -10,49 +10,36 @@ describe Resolvers::MergeRequestResolver do
set(:other_project) { create(:project, :repository) }
set(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) }
let(:full_path) { project.full_path }
let(:iid_1) { merge_request_1.iid }
let(:iid_2) { merge_request_2.iid }
let(:other_full_path) { other_project.full_path }
let(:other_iid) { other_merge_request.iid }
describe '#resolve' do
it 'batch-resolves merge requests by target project full path and IID' do
path = full_path # avoid database query
result = batch(max_queries: 2) do
[resolve_mr(path, iid_1), resolve_mr(path, iid_2)]
[resolve_mr(project, iid_1), resolve_mr(project, iid_2)]
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
end
it 'can batch-resolve merge requests from different projects' do
path = project.full_path # avoid database queries
other_path = other_full_path
result = batch(max_queries: 3) do
[resolve_mr(path, iid_1), resolve_mr(path, iid_2), resolve_mr(other_path, other_iid)]
[resolve_mr(project, iid_1), resolve_mr(project, iid_2), resolve_mr(other_project, other_iid)]
end
expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
end
it 'resolves an unknown iid to nil' do
result = batch { resolve_mr(full_path, -1) }
expect(result).to be_nil
end
it 'resolves a known iid for an unknown full_path to nil' do
result = batch { resolve_mr('unknown/project', iid_1) }
result = batch { resolve_mr(project, -1) }
expect(result).to be_nil
end
end
def resolve_mr(full_path, iid)
resolve(described_class, args: { full_path: full_path, iid: iid })
def resolve_mr(project, iid)
resolve(described_class, obj: project, args: { iid: iid })
end
end
......@@ -2,4 +2,13 @@ require 'spec_helper'
describe GitlabSchema.types['Project'] do
it { expect(described_class.graphql_name).to eq('Project') }
describe 'nested merge request' do
it { expect(described_class).to have_graphql_field(:merge_request) }
it 'authorizes the merge request' do
expect(described_class.fields['mergeRequest'])
.to require_graphql_authorizations(:read_merge_request)
end
end
end
......@@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
it { is_expected.to have_graphql_fields(:project, :merge_request, :echo) }
it { is_expected.to have_graphql_fields(:project, :echo) }
describe 'project field' do
subject { described_class.fields['project'] }
......@@ -20,18 +20,4 @@ describe GitlabSchema.types['Query'] do
is_expected.to require_graphql_authorizations(:read_project)
end
end
describe 'merge_request field' do
subject { described_class.fields['mergeRequest'] }
it 'finds MRs by project and IID' do
is_expected.to have_graphql_arguments(:full_path, :iid)
is_expected.to have_graphql_type(Types::MergeRequestType)
is_expected.to have_graphql_resolver(Resolvers::MergeRequestResolver)
end
it 'authorizes with read_merge_request' do
is_expected.to require_graphql_authorizations(:read_merge_request)
end
end
end
......@@ -93,14 +93,14 @@ describe('Multi-file editor commit sidebar list item', () => {
describe('is active', () => {
it('does not add active class when dont keys match', () => {
expect(vm.$el.classList).not.toContain('is-active');
expect(vm.$el.querySelector('.is-active')).toBe(null);
});
it('adds active class when keys match', done => {
vm.keyPrefix = 'staged';
vm.$nextTick(() => {
expect(vm.$el.classList).toContain('is-active');
expect(vm.$el.querySelector('.is-active')).not.toBe(null);
done();
});
......
......@@ -16,6 +16,7 @@ describe('Multi-file editor commit sidebar list', () => {
iconName: 'staged',
action: 'stageAllChanges',
actionBtnText: 'stage all',
actionBtnIcon: 'history',
itemActionComponent: 'stage-button',
activeFileKey: 'staged-testing',
keyPrefix: 'staged',
......@@ -42,7 +43,7 @@ describe('Multi-file editor commit sidebar list', () => {
});
it('renders list', () => {
expect(vm.$el.querySelectorAll('li').length).toBe(1);
expect(vm.$el.querySelectorAll('.multi-file-commit-list > li').length).toBe(1);
});
});
......
......@@ -39,7 +39,7 @@ describe('IDE stage file button', () => {
});
it('calls store with discard button', () => {
vm.$el.querySelectorAll('.btn')[1].click();
vm.$el.querySelector('.dropdown-menu button').click();
expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path);
});
......
......@@ -111,7 +111,7 @@ describe('RepoCommitSection', () => {
});
it('renders a commit section', () => {
const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list > li')];
const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles);
expect(changedFileElements.length).toEqual(4);
......@@ -140,22 +140,26 @@ describe('RepoCommitSection', () => {
vm.$el.querySelector('.multi-file-discard-btn .btn').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
1,
);
expect(
vm.$el
.querySelector('.ide-commit-list-container')
.querySelectorAll('.multi-file-commit-list > li').length,
).toBe(1);
done();
});
});
it('discards a single file', done => {
vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click();
vm.$el.querySelector('.multi-file-discard-btn .dropdown-menu button').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe(
1,
);
expect(
vm.$el
.querySelector('.ide-commit-list-container')
.querySelectorAll('.multi-file-commit-list > li').length,
).toBe(1);
done();
});
......
......@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
it 'uses the custom favicon if a favicon appearance is present' do
create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png')
expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.png}
expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/dk.png}
end
end
......
......@@ -478,7 +478,7 @@ describe JiraService do
create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png')
props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title')
expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/uploads/-/system/appearance/favicon/\d+/favicon_main_dk.png$}
expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/uploads/-/system/appearance/favicon/\d+/dk.png$}
end
end
end
require 'spec_helper'
describe 'getting merge request information' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:current_user) { create(:user) }
let(:query) do
attributes = {
'fullPath' => merge_request.project.full_path,
'iid' => merge_request.iid
}
graphql_query_for('mergeRequest', attributes)
end
context 'when the user has access to the merge request' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
it 'returns the merge request' do
expect(graphql_data['mergeRequest']).not_to be_nil
end
# This is a field coming from the `MergeRequestPresenter`
it 'includes a web_url' do
expect(graphql_data['mergeRequest']['webUrl']).to be_present
end
it_behaves_like 'a working graphql query'
end
context 'when the user does not have access to the merge request' do
before do
post_graphql(query, current_user: current_user)
end
it 'returns an empty field' do
post_graphql(query, current_user: current_user)
expect(graphql_data['mergeRequest']).to be_nil
end
it_behaves_like 'a working graphql query'
end
end
......@@ -13,27 +13,76 @@ describe 'getting project information' do
context 'when the user has access to the project' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
it 'includes the project' do
post_graphql(query, current_user: current_user)
expect(graphql_data['project']).not_to be_nil
end
it_behaves_like 'a working graphql query'
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when the user does not have access to the project' do
before do
post_graphql(query, current_user: current_user)
context 'when requesting a nested merge request' do
let(:merge_request) { create(:merge_request, source_project: project) }
let(:merge_request_graphql_data) { graphql_data['project']['mergeRequest'] }
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('mergeRequest', iid: merge_request.iid)
)
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
it 'contains merge request information' do
post_graphql(query, current_user: current_user)
expect(merge_request_graphql_data).not_to be_nil
end
# This is a field coming from the `MergeRequestPresenter`
it 'includes a web_url' do
post_graphql(query, current_user: current_user)
expect(merge_request_graphql_data['webUrl']).to be_present
end
context 'when the user does not have access to the merge request' do
let(:project) { create(:project, :public, :repository) }
it 'returns nil' do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
post_graphql(query)
expect(merge_request_graphql_data).to be_nil
end
end
end
end
context 'when the user does not have access to the project' do
it 'returns an empty field' do
post_graphql(query, current_user: current_user)
expect(graphql_data['project']).to be_nil
end
it_behaves_like 'a working graphql query'
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
end
end
......@@ -34,14 +34,20 @@ module GraphqlHelpers
end
def graphql_query_for(name, attributes = {}, fields = nil)
<<~QUERY
{
#{query_graphql_field(name, attributes, fields)}
}
QUERY
end
def query_graphql_field(name, attributes = {}, fields = nil)
fields ||= all_graphql_fields_for(name.classify)
attributes = attributes_to_graphql(attributes)
<<~QUERY
{
#{name}(#{attributes}) {
#{fields}
}
}
QUERY
end
......@@ -50,12 +56,15 @@ module GraphqlHelpers
return "" unless type
type.fields.map do |name, field|
# We can't guess arguments, so skip fields that require them
next if field.arguments.any?
if scalar?(field)
name
else
"#{name} { #{all_graphql_fields_for(field_type(field))} }"
end
end.join("\n")
end.compact.join("\n")
end
def attributes_to_graphql(attributes)
......
......@@ -13,6 +13,12 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
end
end
RSpec::Matchers.define :have_graphql_field do |field_name|
match do |kls|
expect(kls.fields.keys).to include(GraphqlHelpers.fieldnamerize(field_name))
end
end
RSpec::Matchers.define :have_graphql_arguments do |*expected|
include GraphqlHelpers
......
require 'spec_helper'
RSpec.describe FaviconUploader do
include CarrierWave::Test::Matchers
let(:uploader) { described_class.new(build_stubbed(:user)) }
after do
uploader.remove!
end
def upload_fixture(filename)
fixture_file_upload("spec/fixtures/#{filename}")
end
context 'versions' do
before do
uploader.store!(upload_fixture('dk.png'))
end
it 'has the correct format' do
expect(uploader.favicon_main).to be_format('png')
end
it 'has the correct dimensions' do
expect(uploader.favicon_main).to have_dimensions(32, 32)
end
end
end
......@@ -270,11 +270,7 @@ acorn@^3.0.4:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
acorn@^5.0.0, acorn@^5.3.0:
version "5.5.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
acorn@^5.5.0:
acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0:
version "5.6.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7"
......@@ -4002,14 +3998,10 @@ icss-utils@^2.1.0:
dependencies:
postcss "^6.0.1"
ieee754@^1.1.11:
ieee754@^1.1.11, ieee754@^1.1.4:
version "1.1.11"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455"
ieee754@^1.1.4:
version "1.1.8"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
iferr@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
......@@ -6247,11 +6239,7 @@ preserve@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
prettier@^1.11.1:
prettier@1.12.1, prettier@^1.11.1:
version "1.12.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
......
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