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 ...@@ -77,3 +77,4 @@ eslint-report.html
/.rspec /.rspec
/plugins/* /plugins/*
/.gitlab_pages_secret /.gitlab_pages_secret
package-lock.json
...@@ -123,7 +123,7 @@ gl.issueBoards.BoardsStore = { ...@@ -123,7 +123,7 @@ gl.issueBoards.BoardsStore = {
if (!issueTo) { if (!issueTo) {
// Check if target list assignee is already present in this issue // Check if target list assignee is already present in this issue
if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && if ((listTo.type === 'assignee' && listFrom.type === 'assignee') &&
issue.findAssignee(listTo.assignee)) { issue.findAssignee(listTo.assignee)) {
const targetIssue = listTo.findIssue(issue.id); const targetIssue = listTo.findIssue(issue.id);
targetIssue.removeAssignee(listFrom.assignee); targetIssue.removeAssignee(listFrom.assignee);
} else { } else {
...@@ -144,7 +144,7 @@ gl.issueBoards.BoardsStore = { ...@@ -144,7 +144,7 @@ gl.issueBoards.BoardsStore = {
issue.removeAssignee(listFrom.assignee); issue.removeAssignee(listFrom.assignee);
listFrom.removeIssue(issue); listFrom.removeIssue(issue);
} else if ((listTo.type !== 'label' && listFrom.type === 'assignee') || } else if ((listTo.type !== 'label' && listFrom.type === 'assignee') ||
(listTo.type !== 'assignee' && listFrom.type === 'label')) { (listTo.type !== 'assignee' && listFrom.type === 'label')) {
listFrom.removeIssue(issue); listFrom.removeIssue(issue);
} }
}, },
...@@ -162,13 +162,9 @@ gl.issueBoards.BoardsStore = { ...@@ -162,13 +162,9 @@ gl.issueBoards.BoardsStore = {
}); });
return filteredList[0]; return filteredList[0];
}, },
updateFiltersUrl (replaceState = false) { updateFiltersUrl () {
if (replaceState) { window.history.pushState(null, null, `?${this.filter.path}`);
window.history.replaceState(null, null, `?${this.filter.path}`); }
} else {
window.history.pushState(null, null, `?${this.filter.path}`);
}
},
}; };
boardsStoreEE.initEESpecific(gl.issueBoards.BoardsStore); boardsStoreEE.initEESpecific(gl.issueBoards.BoardsStore);
...@@ -34,6 +34,10 @@ export default { ...@@ -34,6 +34,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
actionBtnIcon: {
type: String,
required: true,
},
itemActionComponent: { itemActionComponent: {
type: String, type: String,
required: true, required: true,
...@@ -53,26 +57,21 @@ export default { ...@@ -53,26 +57,21 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
showActionButton: false,
};
},
computed: { computed: {
titleText() { titleText() {
return sprintf(__('%{title} changes'), { return sprintf(__('%{title} changes'), {
title: this.title, title: this.title,
}); });
}, },
filesLength() {
return this.fileList.length;
},
}, },
methods: { methods: {
...mapActions(['stageAllChanges', 'unstageAllChanges']), ...mapActions(['stageAllChanges', 'unstageAllChanges']),
actionBtnClicked() { actionBtnClicked() {
this[this.action](); this[this.action]();
}, },
setShowActionButton(show) {
this.showActionButton = show;
},
}, },
}; };
</script> </script>
...@@ -83,8 +82,6 @@ export default { ...@@ -83,8 +82,6 @@ export default {
> >
<header <header
class="multi-file-commit-panel-header" class="multi-file-commit-panel-header"
@mouseenter="setShowActionButton(true)"
@mouseleave="setShowActionButton(false)"
> >
<div <div
class="multi-file-commit-panel-header-title" class="multi-file-commit-panel-header-title"
...@@ -95,24 +92,40 @@ export default { ...@@ -95,24 +92,40 @@ export default {
:size="18" :size="18"
/> />
{{ titleText }} {{ titleText }}
<span <div class="d-flex ml-auto">
v-show="!showActionButton" <button
class="ide-commit-file-count" v-tooltip
> v-show="filesLength"
{{ fileList.length }} :class="{
</span> 'd-flex': filesLength
<button }"
v-show="showActionButton" :title="actionBtnText"
type="button" type="button"
class="btn btn-blank btn-link ide-staged-action-btn" class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center"
@click="actionBtnClicked" data-placement="bottom"
> data-container="body"
{{ actionBtnText }} data-boundary="viewport"
</button> @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> </div>
</header> </header>
<ul <ul
v-if="fileList.length" v-if="filesLength"
class="multi-file-commit-list list-unstyled append-bottom-0" class="multi-file-commit-list list-unstyled append-bottom-0"
> >
<li <li
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import StageButton from './stage_button.vue'; import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue'; import UnstageButton from './unstage_button.vue';
...@@ -11,6 +12,9 @@ export default { ...@@ -11,6 +12,9 @@ export default {
StageButton, StageButton,
UnstageButton, UnstageButton,
}, },
directives: {
tooltip,
},
props: { props: {
file: { file: {
type: Object, type: Object,
...@@ -50,6 +54,9 @@ export default { ...@@ -50,6 +54,9 @@ export default {
isActive() { isActive() {
return this.activeFileKey === this.fullKey; return this.activeFileKey === this.fullKey;
}, },
tooltipTitle() {
return this.file.path === this.file.name ? '' : this.file.path;
},
}, },
methods: { methods: {
...mapActions([ ...mapActions([
...@@ -81,29 +88,30 @@ export default { ...@@ -81,29 +88,30 @@ export default {
</script> </script>
<template> <template>
<div <div class="multi-file-commit-list-item position-relative">
:class="{
'is-active': isActive
}"
class="multi-file-commit-list-item"
>
<button <button
v-tooltip
:title="tooltipTitle"
:class="{
'is-active': isActive
}"
type="button" type="button"
class="multi-file-commit-list-path" class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0"
@dblclick="fileAction" @dblclick="fileAction"
@click="openFileInEditor" @click="openFileInEditor"
> >
<span class="multi-file-commit-list-file-path"> <span class="multi-file-commit-list-file-path d-flex align-items-center">
<icon <icon
:name="iconName" :name="iconName"
:size="16" :size="16"
:css-classes="iconClass" :css-classes="iconClass"
/>{{ file.path }} />{{ file.name }}
</span> </span>
</button> </button>
<component <component
:is="actionComponent" :is="actionComponent"
:path="file.path" :path="file.path"
class="d-flex position-absolute"
/> />
</div> </div>
</template> </template>
...@@ -25,15 +25,17 @@ export default { ...@@ -25,15 +25,17 @@ export default {
<template> <template>
<div <div
v-once v-once
class="multi-file-discard-btn" class="multi-file-discard-btn dropdown"
> >
<button <button
v-tooltip v-tooltip
:aria-label="__('Stage changes')" :aria-label="__('Stage changes')"
:title="__('Stage changes')" :title="__('Stage changes')"
type="button" 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-container="body"
data-boundary="viewport"
data-placement="bottom"
@click.stop="stageChange(path)" @click.stop="stageChange(path)"
> >
<icon <icon
...@@ -43,17 +45,31 @@ export default { ...@@ -43,17 +45,31 @@ export default {
</button> </button>
<button <button
v-tooltip v-tooltip
:aria-label="__('Discard changes')" :title="__('More actions')"
:title="__('Discard changes')"
type="button" type="button"
class="btn btn-blank" class="btn btn-blank d-flex align-items-center"
data-container="body" data-container="body"
@click.stop="discardFileChanges(path)" data-boundary="viewport"
data-placement="bottom"
data-toggle="dropdown"
data-display="static"
> >
<icon <icon
:size="12" :size="12"
name="remove" name="more"
/> />
</button> </button>
<div class="dropdown-menu dropdown-menu-right">
<ul>
<li>
<button
type="button"
@click.stop="discardFileChanges(path)"
>
{{ __('Discard changes') }}
</button>
</li>
</ul>
</div>
</div> </div>
</template> </template>
...@@ -32,8 +32,10 @@ export default { ...@@ -32,8 +32,10 @@ export default {
:aria-label="__('Unstage changes')" :aria-label="__('Unstage changes')"
:title="__('Unstage changes')" :title="__('Unstage changes')"
type="button" type="button"
class="btn btn-blank" class="btn btn-blank d-flex align-items-center"
data-container="body" data-container="body"
data-boundary="viewport"
data-placement="bottom"
@click="unstageChange(path)" @click="unstageChange(path)"
> >
<icon <icon
......
...@@ -93,23 +93,25 @@ export default { ...@@ -93,23 +93,25 @@ export default {
:title="__('Unstaged')" :title="__('Unstaged')"
:key-prefix="$options.stageKeys.unstaged" :key-prefix="$options.stageKeys.unstaged"
:file-list="changedFiles" :file-list="changedFiles"
:action-btn-text="__('Stage all')" :action-btn-text="__('Stage all changes')"
:active-file-key="activeFileKey" :active-file-key="activeFileKey"
class="is-first"
icon-name="unstaged"
action="stageAllChanges" action="stageAllChanges"
action-btn-icon="mobile-issue-close"
item-action-component="stage-button" item-action-component="stage-button"
class="is-first"
icon-name="unstaged"
/> />
<commit-files-list <commit-files-list
:title="__('Staged')" :title="__('Staged')"
:key-prefix="$options.stageKeys.staged" :key-prefix="$options.stageKeys.staged"
:file-list="stagedFiles" :file-list="stagedFiles"
:action-btn-text="__('Unstage all')" :action-btn-text="__('Unstage all changes')"
:staged-list="true" :staged-list="true"
:active-file-key="activeFileKey" :active-file-key="activeFileKey"
icon-name="staged"
action="unstageAllChanges" action="unstageAllChanges"
action-btn-icon="history"
item-action-component="unstage-button" item-action-component="unstage-button"
icon-name="staged"
/> />
</template> </template>
<empty-state <empty-state
......
...@@ -870,3 +870,4 @@ $font-family-sans-serif: $regular_font; ...@@ -870,3 +870,4 @@ $font-family-sans-serif: $regular_font;
$font-family-monospace: $monospace_font; $font-family-monospace: $monospace_font;
$input-line-height: 20px; $input-line-height: 20px;
$btn-line-height: 20px; $btn-line-height: 20px;
$table-accent-bg: $gray-light;
...@@ -280,7 +280,7 @@ ...@@ -280,7 +280,7 @@
width: 150px; width: 150px;
flex-shrink: 0; flex-shrink: 0;
.label { .badge {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 100%; max-width: 100%;
......
...@@ -540,36 +540,12 @@ ...@@ -540,36 +540,12 @@
margin-right: -$grid-size; margin-right: -$grid-size;
min-height: 60px; min-height: 60px;
.multi-file-commit-list-item {
margin-left: 0;
margin-right: 0;
}
&.form-text.text-muted { &.form-text.text-muted {
margin-left: 0; margin-left: 0;
right: 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,
.multi-file-addition-solid { .multi-file-addition-solid {
color: $green-500; color: $green-500;
...@@ -599,7 +575,7 @@ ...@@ -599,7 +575,7 @@
} }
} }
.multi-file-commit-list-item, .multi-file-commit-list-path,
.ide-file-list .file { .ide-file-list .file {
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -616,11 +592,9 @@ ...@@ -616,11 +592,9 @@
} }
.multi-file-commit-list-path { .multi-file-commit-list-path {
padding: 0; &.is-active {
background: none; background-color: $white-normal;
border: 0; }
text-align: left;
width: 100%;
&:hover, &:hover,
&:focus { &:focus {
...@@ -635,7 +609,7 @@ ...@@ -635,7 +609,7 @@
} }
.multi-file-commit-list-file-path { .multi-file-commit-list-file-path {
@include str-truncated(100%); @include str-truncated(calc(100% - 30px));
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
...@@ -646,6 +620,16 @@ ...@@ -646,6 +620,16 @@
} }
} }
.multi-file-discard-btn {
top: 4px;
right: 8px;
bottom: 4px;
svg {
top: 0;
}
}
.multi-file-commit-form { .multi-file-commit-form {
position: relative; position: relative;
background-color: $white-light; background-color: $white-light;
...@@ -840,18 +824,20 @@ ...@@ -840,18 +824,20 @@
} }
.ide-staged-action-btn { .ide-staged-action-btn {
margin-left: auto; width: 22px;
line-height: 22px; margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
> svg {
top: 0;
}
} }
.ide-commit-file-count { .ide-commit-file-count {
min-width: 22px; min-width: 22px;
margin-left: auto;
background-color: $gray-light; background-color: $gray-light;
border-radius: $border-radius-default;
border: 1px solid $white-dark; border: 1px solid $white-dark;
line-height: 20px;
text-align: center;
} }
.ide-commit-radios { .ide-commit-radios {
......
...@@ -123,9 +123,9 @@ class Projects::MilestonesController < Projects::ApplicationController ...@@ -123,9 +123,9 @@ class Projects::MilestonesController < Projects::ApplicationController
def search_params def search_params
if request.format.json? && @project.group && can?(current_user, :read_group, @project.group) 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 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
end end
module Resolvers module Resolvers
class MergeRequestResolver < BaseResolver class MergeRequestResolver < BaseResolver
prepend FullPathResolver
type Types::ProjectType, null: true
argument :iid, GraphQL::ID_TYPE, argument :iid, GraphQL::ID_TYPE,
required: true, required: true,
description: 'The IID of the merge request, e.g., "1"' description: 'The IID of the merge request, e.g., "1"'
def resolve(full_path:, iid:) type Types::MergeRequestType, null: true
project = model_by_full_path(Project, full_path)
alias_method :project, :object
def resolve(iid:)
return unless project.present? return unless project.present?
BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader| BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader|
......
...@@ -61,5 +61,12 @@ module Types ...@@ -61,5 +61,12 @@ module Types
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true
field :only_allow_merge_if_all_discussions_are_resolved, 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 :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
end end
...@@ -9,13 +9,6 @@ module Types ...@@ -9,13 +9,6 @@ module Types
authorize :read_project authorize :read_project
end 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 field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new
end end
end end
class FaviconUploader < AttachmentUploader class FaviconUploader < AttachmentUploader
EXTENSION_WHITELIST = %w[png ico].freeze 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 def extension_whitelist
EXTENSION_WHITELIST EXTENSION_WHITELIST
end end
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
= f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label' = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label'
.col-sm-10 .col-sm-10
- if @appearance.favicon? - 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? - if @appearance.persisted?
%br %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" = 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 @@ ...@@ -33,9 +33,9 @@
= f.hidden_field :favicon_cache = f.hidden_field :favicon_cache
= f.file_field :favicon, class: '' = f.file_field :favicon, class: ''
.hint .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 %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 } = render partial: 'admin/appearances/system_header_footer_form', locals: { form: f }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
None None
%a{ href: "#", %a{ href: "#",
"v-for" => "label in issue.labels" } "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 }} {{ label.title }}
- if can_admin_issue? - if can_admin_issue?
.selectbox .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 # rubocop:disable Lint/RescueException
# Remove this entire initializer when we are at rails 5.0. # Remove this monkey-patch when all lock_version values are converted from NULLs to zeros.
# This file fixes the bug (see below) which has been fixed in the upstream. # See https://gitlab.com/gitlab-org/gitlab-ce/issues/25228
unless Gitlab.rails5? module ActiveRecord
# This patch fixes https://github.com/rails/rails/issues/26024 module Locking
# TODO: Remove it when it's no longer necessary module Optimistic
# We overwrite this method because we don't want to have default value
module ActiveRecord # for newly created records
module Locking def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
module Optimistic super
# We overwrite this method because we don't want to have default value end
# for newly created records
def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
super
end
def _update_record(attribute_names = self.attribute_names) #:nodoc: def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled? return super unless locking_enabled?
return 0 if attribute_names.empty? 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 # This line is added as a patch
previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0 previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
increment_lock increment_lock
attribute_names += [lock_col] attribute_names += [lock_col]
attribute_names.uniq! attribute_names.uniq!
begin begin
relation = self.class.unscoped relation = self.class.unscoped
affected_rows = relation.where( affected_rows = relation.where(
self.class.primary_key => id, self.class.primary_key => id,
lock_col => previous_lock_value lock_col => previous_lock_value
).update_all( ).update_all(
attributes_for_update(attribute_names).map do |name| attributes_for_update(attribute_names).map do |name|
[name, _read_attribute(name)] [name, _read_attribute(name)]
end.to_h end.to_h
) )
unless affected_rows == 1 unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update") raise ActiveRecord::StaleObjectError.new(self, "update")
end end
affected_rows affected_rows
# If something went wrong, revert the version. # If something went wrong, revert the version.
rescue Exception rescue Exception
send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
raise raise
end
end end
end
# This is patched because we need it to query `lock_version IS NULL` # This is patched because we need it to query `lock_version IS NULL`
# rather than `lock_version = 0` whenever lock_version is NULL. # rather than `lock_version = 0` whenever lock_version is NULL.
def relation_for_destroy def relation_for_destroy
return super unless locking_enabled? return super unless locking_enabled?
column_name = self.class.locking_column column_name = self.class.locking_column
super.where(self.class.arel_table[column_name].eq(self[column_name])) super.where(self.class.arel_table[column_name].eq(self[column_name]))
end
end 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` def serialize(value)
# rather than `0` super
end
end
else
class LockingType < SimpleDelegator class LockingType < SimpleDelegator
def type_cast_from_database(value) def type_cast_from_database(value)
super super
......
...@@ -29,9 +29,7 @@ curl --data "value=100" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://g ...@@ -29,9 +29,7 @@ curl --data "value=100" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://g
## Available queries ## Available queries
A first iteration of a GraphQL API includes only 2 queries: `project` and 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.
`merge_request` and only returns scalar fields, or fields of the type `Project`
or `MergeRequest`.
## GraphiQL ## GraphiQL
......
...@@ -43,3 +43,17 @@ yarn prettier-all-save ...@@ -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). 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`. 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: ...@@ -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 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. 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: On your GitLab server:
......
...@@ -28,15 +28,27 @@ class BoardsStoreEE { ...@@ -28,15 +28,27 @@ class BoardsStoreEE {
this.initBoardFilters(); 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() { initBoardFilters() {
const updateFilterPath = (key, value) => { const updateFilterPath = (key, value) => {
if (!value) return; if (!value) return;
const querystring = `${key}=${value}`; const querystring = `${key}=${value}`;
this.store.filter.path = [querystring].concat( this.store.filter.path = [querystring]
this.store.filter.path.split('&').filter(param => param.match(new RegExp(`^${key}=(.*)$`, 'g')) === null), .concat(
).join('&'); this.store.filter.path
.split('&')
.filter(param => param.match(new RegExp(`^${key}=(.*)$`, 'g')) === null),
)
.join('&');
}; };
let milestoneTitle = this.store.boardConfig.milestoneTitle; let milestoneTitle = this.store.boardConfig.milestoneTitle;
...@@ -64,7 +76,7 @@ class BoardsStoreEE { ...@@ -64,7 +76,7 @@ class BoardsStoreEE {
} }
const filterPath = this.store.filter.path.split('&'); 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 labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`; const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param); const labelIndex = filterPath.indexOf(param);
...@@ -85,7 +97,12 @@ class BoardsStoreEE { ...@@ -85,7 +97,12 @@ class BoardsStoreEE {
} }
addPromotion() { 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({ this.store.addList({
id: 'promotion', id: 'promotion',
......
...@@ -65,7 +65,7 @@ module Banzai ...@@ -65,7 +65,7 @@ module Banzai
# We don't support IID lookups for group milestones, because IIDs can # We don't support IID lookups for group milestones, because IIDs can
# clash between group and project milestones. # clash between group and project milestones.
if project.group && !params[:iid] 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 end
MilestonesFinder.new(finder_params).find_by(params) MilestonesFinder.new(finder_params).find_by(params)
......
...@@ -2,7 +2,7 @@ module Gitlab ...@@ -2,7 +2,7 @@ module Gitlab
class Favicon class Favicon
class << self class << self
def main def main
return appearance_favicon.favicon_main.url if appearance_favicon.exists? return appearance_favicon.url if appearance_favicon.exists?
image_name = image_name =
if Gitlab::Utils.to_boolean(ENV['CANARY']) if Gitlab::Utils.to_boolean(ENV['CANARY'])
......
...@@ -9,6 +9,8 @@ const getStagedFiles = require('./frontend_script_utils').getStagedFiles; ...@@ -9,6 +9,8 @@ const getStagedFiles = require('./frontend_script_utils').getStagedFiles;
const mode = process.argv[2] || 'check'; const mode = process.argv[2] || 'check';
const shouldSave = mode === 'save' || mode === 'save-all'; const shouldSave = mode === 'save' || mode === 'save-all';
const allFiles = mode === 'check-all' || 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 = { const config = {
patterns: ['**/*.js', '**/*.vue', '**/*.scss'], patterns: ['**/*.js', '**/*.vue', '**/*.scss'],
...@@ -39,9 +41,10 @@ prettierIgnore.add( ...@@ -39,9 +41,10 @@ prettierIgnore.add(
const availableExtensions = Object.keys(config.parsers); 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) {
if (!stagedFiles.length || (stagedFiles.length === 1 && !stagedFiles[0])) { if (!stagedFiles.length || (stagedFiles.length === 1 && !stagedFiles[0])) {
...@@ -60,6 +63,13 @@ if (allFiles) { ...@@ -60,6 +63,13 @@ if (allFiles) {
const patterns = config.patterns; const patterns = config.patterns;
const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`; const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`;
files = glob.sync(globPattern, { ignore }).filter(f => allFiles || stagedFiles.includes(f)); 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 { } else {
files = stagedFiles.filter(f => availableExtensions.includes(f.split('.').pop())); files = stagedFiles.filter(f => availableExtensions.includes(f.split('.').pop()));
} }
...@@ -73,12 +83,11 @@ if (!files.length) { ...@@ -73,12 +83,11 @@ if (!files.length) {
console.log(`${shouldSave ? 'Updating' : 'Checking'} ${files.length} file(s)`); console.log(`${shouldSave ? 'Updating' : 'Checking'} ${files.length} file(s)`);
prettier files.forEach(file => {
.resolveConfig('.') try {
.then(options => { prettier
console.log('Found options : ', options); .resolveConfig(file)
files.forEach(file => { .then(options => {
try {
const fileExtension = file.split('.').pop(); const fileExtension = file.split('.').pop();
Object.assign(options, { Object.assign(options, {
parser: config.parsers[fileExtension], parser: config.parsers[fileExtension],
...@@ -101,17 +110,17 @@ prettier ...@@ -101,17 +110,17 @@ prettier
} }
console.log(`Prettify Manually : ${file}`); console.log(`Prettify Manually : ${file}`);
} }
} catch (error) { })
didError = true; .catch(e => {
console.log(`\n\nError with ${file}: ${error.message}`); console.log(`Error on loading the Config File: ${e.message}`);
} process.exit(1);
}); });
} catch (error) {
if (didWarn || didError) { didError = true;
process.exit(1); console.log(`\n\nError with ${file}: ${error.message}`);
} }
}) });
.catch(e => {
console.log(`Error on loading the Config File: ${e.message}`); if (didWarn || didError) {
process.exit(1); process.exit(1);
}); }
...@@ -580,23 +580,6 @@ describe UploadsController do ...@@ -580,23 +580,6 @@ describe UploadsController do
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
end end
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 end
end end
...@@ -34,7 +34,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do ...@@ -34,7 +34,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests wait_for_requests
expect(page).to have_selector('span.badge', text: label.title) expect(page).to have_selector('.badge', text: label.title)
end end
end end
...@@ -45,7 +45,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do ...@@ -45,7 +45,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do
wait_for_requests 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
end end
......
...@@ -10,49 +10,36 @@ describe Resolvers::MergeRequestResolver do ...@@ -10,49 +10,36 @@ describe Resolvers::MergeRequestResolver do
set(:other_project) { create(:project, :repository) } set(:other_project) { create(:project, :repository) }
set(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) } 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_1) { merge_request_1.iid }
let(:iid_2) { merge_request_2.iid } let(:iid_2) { merge_request_2.iid }
let(:other_full_path) { other_project.full_path }
let(:other_iid) { other_merge_request.iid } let(:other_iid) { other_merge_request.iid }
describe '#resolve' do describe '#resolve' do
it 'batch-resolves merge requests by target project full path and IID' 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 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 end
expect(result).to contain_exactly(merge_request_1, merge_request_2) expect(result).to contain_exactly(merge_request_1, merge_request_2)
end end
it 'can batch-resolve merge requests from different projects' do 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 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 end
expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
end end
it 'resolves an unknown iid to nil' do it 'resolves an unknown iid to nil' do
result = batch { resolve_mr(full_path, -1) } result = batch { resolve_mr(project, -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) }
expect(result).to be_nil expect(result).to be_nil
end end
end end
def resolve_mr(full_path, iid) def resolve_mr(project, iid)
resolve(described_class, args: { full_path: full_path, iid: iid }) resolve(described_class, obj: project, args: { iid: iid })
end end
end end
...@@ -2,4 +2,13 @@ require 'spec_helper' ...@@ -2,4 +2,13 @@ require 'spec_helper'
describe GitlabSchema.types['Project'] do describe GitlabSchema.types['Project'] do
it { expect(described_class.graphql_name).to eq('Project') } 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 end
...@@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do ...@@ -5,7 +5,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query') expect(described_class.graphql_name).to eq('Query')
end 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 describe 'project field' do
subject { described_class.fields['project'] } subject { described_class.fields['project'] }
...@@ -20,18 +20,4 @@ describe GitlabSchema.types['Query'] do ...@@ -20,18 +20,4 @@ describe GitlabSchema.types['Query'] do
is_expected.to require_graphql_authorizations(:read_project) is_expected.to require_graphql_authorizations(:read_project)
end end
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 end
...@@ -93,14 +93,14 @@ describe('Multi-file editor commit sidebar list item', () => { ...@@ -93,14 +93,14 @@ describe('Multi-file editor commit sidebar list item', () => {
describe('is active', () => { describe('is active', () => {
it('does not add active class when dont keys match', () => { 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 => { it('adds active class when keys match', done => {
vm.keyPrefix = 'staged'; vm.keyPrefix = 'staged';
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.classList).toContain('is-active'); expect(vm.$el.querySelector('.is-active')).not.toBe(null);
done(); done();
}); });
......
...@@ -16,6 +16,7 @@ describe('Multi-file editor commit sidebar list', () => { ...@@ -16,6 +16,7 @@ describe('Multi-file editor commit sidebar list', () => {
iconName: 'staged', iconName: 'staged',
action: 'stageAllChanges', action: 'stageAllChanges',
actionBtnText: 'stage all', actionBtnText: 'stage all',
actionBtnIcon: 'history',
itemActionComponent: 'stage-button', itemActionComponent: 'stage-button',
activeFileKey: 'staged-testing', activeFileKey: 'staged-testing',
keyPrefix: 'staged', keyPrefix: 'staged',
...@@ -42,7 +43,7 @@ describe('Multi-file editor commit sidebar list', () => { ...@@ -42,7 +43,7 @@ describe('Multi-file editor commit sidebar list', () => {
}); });
it('renders 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', () => { ...@@ -39,7 +39,7 @@ describe('IDE stage file button', () => {
}); });
it('calls store with discard 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); expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path);
}); });
......
...@@ -111,7 +111,7 @@ describe('RepoCommitSection', () => { ...@@ -111,7 +111,7 @@ describe('RepoCommitSection', () => {
}); });
it('renders a commit section', () => { 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); const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles);
expect(changedFileElements.length).toEqual(4); expect(changedFileElements.length).toEqual(4);
...@@ -140,22 +140,26 @@ describe('RepoCommitSection', () => { ...@@ -140,22 +140,26 @@ describe('RepoCommitSection', () => {
vm.$el.querySelector('.multi-file-discard-btn .btn').click(); vm.$el.querySelector('.multi-file-discard-btn .btn').click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe( expect(
1, vm.$el
); .querySelector('.ide-commit-list-container')
.querySelectorAll('.multi-file-commit-list > li').length,
).toBe(1);
done(); done();
}); });
}); });
it('discards a single file', 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(() => { Vue.nextTick(() => {
expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1'); expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe( expect(
1, vm.$el
); .querySelector('.ide-commit-list-container')
.querySelectorAll('.multi-file-commit-list > li').length,
).toBe(1);
done(); done();
}); });
......
...@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Favicon, :request_store do ...@@ -19,7 +19,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
it 'uses the custom favicon if a favicon appearance is present' do it 'uses the custom favicon if a favicon appearance is present' do
create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png') 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
end end
......
...@@ -478,7 +478,7 @@ describe JiraService do ...@@ -478,7 +478,7 @@ describe JiraService do
create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png') 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') 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 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 ...@@ -13,27 +13,76 @@ describe 'getting project information' do
context 'when the user has access to the project' do context 'when the user has access to the project' do
before do before do
project.add_developer(current_user) project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end end
it 'includes the project' do it 'includes the project' do
post_graphql(query, current_user: current_user)
expect(graphql_data['project']).not_to be_nil expect(graphql_data['project']).not_to be_nil
end end
it_behaves_like 'a working graphql query' it_behaves_like 'a working graphql query' do
end before do
post_graphql(query, current_user: current_user)
end
end
context 'when the user does not have access to the project' do context 'when requesting a nested merge request' do
before do let(:merge_request) { create(:merge_request, source_project: project) }
post_graphql(query, current_user: current_user) 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
end
context 'when the user does not have access to the project' do
it 'returns an empty field' do it 'returns an empty field' do
post_graphql(query, current_user: current_user) post_graphql(query, current_user: current_user)
expect(graphql_data['project']).to be_nil expect(graphql_data['project']).to be_nil
end 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
end end
...@@ -34,14 +34,20 @@ module GraphqlHelpers ...@@ -34,14 +34,20 @@ module GraphqlHelpers
end end
def graphql_query_for(name, attributes = {}, fields = nil) 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) fields ||= all_graphql_fields_for(name.classify)
attributes = attributes_to_graphql(attributes) attributes = attributes_to_graphql(attributes)
<<~QUERY <<~QUERY
{
#{name}(#{attributes}) { #{name}(#{attributes}) {
#{fields} #{fields}
} }
}
QUERY QUERY
end end
...@@ -50,12 +56,15 @@ module GraphqlHelpers ...@@ -50,12 +56,15 @@ module GraphqlHelpers
return "" unless type return "" unless type
type.fields.map do |name, field| 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) if scalar?(field)
name name
else else
"#{name} { #{all_graphql_fields_for(field_type(field))} }" "#{name} { #{all_graphql_fields_for(field_type(field))} }"
end end
end.join("\n") end.compact.join("\n")
end end
def attributes_to_graphql(attributes) def attributes_to_graphql(attributes)
......
...@@ -13,6 +13,12 @@ RSpec::Matchers.define :have_graphql_fields do |*expected| ...@@ -13,6 +13,12 @@ RSpec::Matchers.define :have_graphql_fields do |*expected|
end end
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| RSpec::Matchers.define :have_graphql_arguments do |*expected|
include GraphqlHelpers 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: ...@@ -270,11 +270,7 @@ acorn@^3.0.4:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
acorn@^5.0.0, acorn@^5.3.0: acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0:
version "5.5.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9"
acorn@^5.5.0:
version "5.6.2" version "5.6.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7"
...@@ -4002,14 +3998,10 @@ icss-utils@^2.1.0: ...@@ -4002,14 +3998,10 @@ icss-utils@^2.1.0:
dependencies: dependencies:
postcss "^6.0.1" postcss "^6.0.1"
ieee754@^1.1.11: ieee754@^1.1.11, ieee754@^1.1.4:
version "1.1.11" version "1.1.11"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" 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: iferr@^0.1.5:
version "0.1.5" version "0.1.5"
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
...@@ -6247,11 +6239,7 @@ preserve@^0.2.0: ...@@ -6247,11 +6239,7 @@ preserve@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
prettier@1.11.1: prettier@1.12.1, prettier@^1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
prettier@^1.11.1:
version "1.12.1" version "1.12.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" 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