Commit 66690c55 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 566a3846 dda21b59
3f5e218def93024f3aafe590c22cd1b29f744105
cf1ceffbf8056281864432c8f472140fb83f5949
......@@ -19,6 +19,7 @@ import Api from '~/api';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { s__, __ } from '~/locale';
import { urlParamsToObject } from '~/lib/utils/common_utils';
......@@ -40,6 +41,7 @@ import {
TH_CREATED_AT_TEST_ID,
TH_SEVERITY_TEST_ID,
TH_PUBLISHED_TEST_ID,
INCIDENT_DETAILS_PATH,
} from '../constants';
const tdClass =
......@@ -111,6 +113,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
inject: [
'projectPath',
'newIssuePath',
......@@ -332,7 +335,10 @@ export default {
return Boolean(assignees.nodes?.length);
},
navigateToIncidentDetails({ iid }) {
return visitUrl(joinPaths(this.issuePath, iid));
const path = this.glFeatures.issuesIncidentDetails
? joinPaths(this.issuePath, INCIDENT_DETAILS_PATH)
: this.issuePath;
return visitUrl(joinPaths(path, iid));
},
handlePageChange(page) {
const { startCursor, endCursor } = this.incidents.pageInfo;
......
......@@ -38,3 +38,4 @@ export const DEFAULT_PAGE_SIZE = 20;
export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' };
export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' };
export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' };
export const INCIDENT_DETAILS_PATH = 'incident';
......@@ -44,6 +44,7 @@ export const checkPageAndAction = (page, action) => {
return pagePath === page && actionPath === action;
};
export const isInIncidentPage = () => checkPageAndAction('issues', 'incident');
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
......
import initRelatedIssues from '~/related_issues';
import initShow from '../../issues/show';
document.addEventListener('DOMContentLoaded', () => {
initShow();
initRelatedIssues();
});
......@@ -12,12 +12,17 @@ import { setTitle } from './utils/title';
import { updateFormAction } from './utils/dom';
import { convertObjectPropsToCamelCase, parseBoolean } from '../lib/utils/common_utils';
import { __ } from '../locale';
import PathLastCommitQuery from './queries/path_last_commit.query.graphql';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
const { dataset } = el;
const { projectPath, projectShortPath, ref, escapedRef, fullName } = dataset;
const router = createRouter(projectPath, escapedRef);
const pathRegex = /-\/tree\/[^/]+\/(.+$)/;
const matches = window.location.href.match(pathRegex);
const currentRoutePath = matches ? matches[1] : '';
apolloProvider.clients.defaultClient.cache.writeData({
data: {
......@@ -29,6 +34,43 @@ export default function setupVueRepositoryList() {
},
});
const initLastCommitApp = () =>
new Vue({
el: document.getElementById('js-last-commit'),
router,
apolloProvider,
render(h) {
return h(LastCommit, {
props: {
currentPath: this.$route.params.path,
},
});
},
});
if (window.gl.startup_graphql_calls) {
const query = window.gl.startup_graphql_calls.find(
call => call.operationName === 'pathLastCommit',
);
query.fetchCall
.then(res => res.json())
.then(res => {
apolloProvider.clients.defaultClient.writeQuery({
query: PathLastCommitQuery,
data: res.data,
variables: {
projectPath,
ref,
path: currentRoutePath,
},
});
})
.catch(() => {})
.finally(() => initLastCommitApp());
} else {
initLastCommitApp();
}
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
});
......@@ -77,20 +119,6 @@ export default function setupVueRepositoryList() {
});
}
// eslint-disable-next-line no-new
new Vue({
el: document.getElementById('js-last-commit'),
router,
apolloProvider,
render(h) {
return h(LastCommit, {
props: {
currentPath: this.$route.params.path,
},
});
},
});
const treeHistoryLinkEl = document.getElementById('js-tree-history-link');
const { historyLink } = treeHistoryLinkEl.dataset;
......
query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
project(fullPath: $projectPath) {
__typename
repository {
__typename
tree(path: $path, ref: $ref) {
__typename
lastCommit {
__typename
sha
title
titleHtml
......@@ -13,15 +17,20 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
authorName
authorGravatar
author {
__typename
name
avatarUrl
webPath
}
signatureHtml
pipelines(ref: $ref, first: 1) {
__typename
edges {
__typename
node {
__typename
detailedStatus {
__typename
detailsPath
icon
tooltip
......
......@@ -14,7 +14,7 @@ import sidebarSubscriptions from './components/subscriptions/sidebar_subscriptio
import SidebarSeverity from './components/severity/sidebar_severity.vue';
import Translate from '../vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
import { isInIssuePage, parseBoolean } from '~/lib/utils/common_utils';
import { isInIssuePage, isInIncidentPage, parseBoolean } from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
......@@ -51,7 +51,7 @@ function mountAssigneesComponent(mediator) {
projectPath: fullPath,
field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'),
issuableType: isInIssuePage() ? 'issue' : 'merge_request',
issuableType: isInIssuePage() || isInIncidentPage() ? 'issue' : 'merge_request',
},
}),
});
......@@ -158,7 +158,7 @@ function mountLockComponent() {
const initialData = JSON.parse(dataNode.innerHTML);
let importStore;
if (isInIssuePage()) {
if (isInIssuePage() || isInIncidentPage()) {
importStore = import(/* webpackChunkName: 'notesStore' */ '~/notes/stores').then(
({ store }) => store,
);
......
# frozen_string_literal: true
class Projects::IncidentsController < Projects::ApplicationController
include IssuableActions
include Gitlab::Utils::StrongMemoize
before_action :authorize_read_issue!
before_action :check_feature_flag, only: [:show]
before_action :load_incident, only: [:show]
before_action do
push_frontend_feature_flag(:issues_incident_details, @project)
end
def index
end
private
def incident
strong_memoize(:incident) do
incident_finder
.execute
.inc_relations_for_view
.iid_in(params[:id])
.without_order
.first
end
end
def load_incident
@issue = incident # needed by rendered view
return render_404 unless can?(current_user, :read_issue, incident)
@noteable = incident
@note = incident.project.notes.new(noteable: issuable)
end
alias_method :issuable, :incident
def incident_finder
IssuesFinder.new(current_user, project_id: @project.id, issue_types: :incident)
end
def serializer
IssueSerializer.new(current_user: current_user, project: incident.project)
end
def check_feature_flag
render_404 unless Feature.enabled?(:issues_incident_details, @project)
end
end
......@@ -239,7 +239,7 @@ class Projects::IssuesController < Projects::ApplicationController
return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by
@issuable = @noteable = @issue ||= @project.issues.includes(author: :status).where(iid: params[:id]).reorder(nil).take!
@issuable = @noteable = @issue ||= @project.issues.inc_relations_for_view.iid_in(params[:id]).without_order.take!
@note = @project.notes.new(noteable: @issuable)
return render_404 unless can?(current_user, :read_issue, @issue)
......
......@@ -55,7 +55,8 @@ module NavHelper
current_path?('projects/merge_requests/conflicts#show') ||
current_path?('issues#show') ||
current_path?('milestones#show') ||
current_path?('issues#designs')
current_path?('issues#designs') ||
current_path?('incidents#show')
end
def admin_monitoring_nav_links
......
# frozen_string_literal: true
module StartupjsHelper
def page_startup_graphql_calls
@graphql_startup_calls
end
def add_page_startup_graphql_call(query, variables = {})
@graphql_startup_calls ||= []
query_str = File.read(File.join(Rails.root, "app/assets/javascripts/#{query}.query.graphql"))
@graphql_startup_calls << { query: query_str, variables: variables }
end
end
......@@ -126,6 +126,7 @@ class Issue < ApplicationRecord
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
scope :service_desk, -> { where(author: ::User.support_bot) }
scope :inc_relations_for_view, -> { includes(author: :status) }
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
......
- return unless page_startup_api_calls.present?
- return unless page_startup_api_calls.present? || page_startup_graphql_calls.present?
= javascript_tag nonce: true do
:plain
var gl = window.gl || {};
gl.startup_calls = #{page_startup_api_calls.to_json};
gl.startup_graphql_calls = #{page_startup_graphql_calls.to_json};
if (gl.startup_calls && window.fetch) {
Object.keys(gl.startup_calls).forEach(apiCall => {
// fetch won’t send cookies in older browsers, unless you set the credentials init option.
......@@ -14,3 +16,21 @@
};
});
}
if (gl.startup_graphql_calls && window.fetch) {
const url = `#{api_graphql_url}`
const opts = {
method: "POST",
headers: { "Content-Type": "application/json", 'X-CSRF-Token': "#{form_authenticity_token}" },
};
gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({
operationName: call.query.match(/^query (.+)\(/)[1],
fetchCall: fetch(url, {
...opts,
credentials: 'same-origin',
body: JSON.stringify(call)
})
}))
}
= render 'projects/issues/new_branch'
= render template: 'projects/issues/show'
- current_route_path = request.fullpath.match(/-\/tree\/[^\/]+\/(.+$)/).to_a[1]
- add_page_startup_graphql_call('repository/queries/path_last_commit', { projectPath: @project.full_path, ref: current_ref, currentRoutePath: current_route_path })
- breadcrumb_title _("Repository")
- @content_class = "limit-container-width" unless fluid_layout
......
---
title: Update GitLab Workhorse to v8.49.0
merge_request: 43999
author:
type: other
---
name: issues_incident_details
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43459
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/257842
type: development
group: group::health
default_enabled: false
......@@ -311,6 +311,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :incidents, only: [:index]
get 'issues/incident/:id' => 'incidents#show', as: :issues_incident
namespace :error_tracking do
resources :projects, only: :index
end
......
......@@ -21,7 +21,7 @@ full list of reference architectures, see
| Consul | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| PostgreSQL | 3 | 8 vCPU, 30 GB memory | n1-standard-8 | m5.2xlarge | D8s v3 |
| PgBouncer | 3 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 2 vCPU, 1.8 GB memory | n1-highcpu-2 | c5.large | F2s v2 |
| Internal load balancing node | 1 | 4 vCPU, 3.6GB memory | n1-highcpu-4 | c5.large | F2s v2 |
| Redis - Cache | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis - Queues / Shared State | 3 | 4 vCPU, 15 GB memory | n1-standard-4 | m5.xlarge | D4s v3 |
| Redis Sentinel - Cache | 3 | 1 vCPU, 1.7 GB memory | g1-small | t2.small | B1MS |
......
......@@ -94,6 +94,8 @@ module Gitlab
[order_value.expr.expressions[0].name.to_s, order_value.direction, order_value.expr]
elsif ordering_by_similarity?(order_value)
['similarity', order_value.direction, order_value.expr]
elsif ordering_by_case?(order_value)
[order_value.expr.case.name.to_s, order_value.direction, order_value.expr]
else
[order_value.expr.name, order_value.direction, nil]
end
......@@ -108,6 +110,11 @@ module Gitlab
def ordering_by_similarity?(order_value)
Gitlab::Database::SimilarityScore.order_by_similarity?(order_value)
end
# determine if ordering using CASE
def ordering_by_case?(order_value)
order_value.expr.is_a?(Arel::Nodes::Case)
end
end
end
end
......
......@@ -3,42 +3,127 @@
require 'spec_helper'
RSpec.describe Projects::IncidentsController do
let_it_be(:project) { create(:project) }
let_it_be_with_refind(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:anonymous) { nil }
before_all do
project.add_guest(guest)
project.add_developer(developer)
end
before do
sign_in(user) if user
end
subject { make_request }
shared_examples 'not found' do
include_examples 'returning response status', :not_found
end
shared_examples 'login required' do
it 'redirects to the login page' do
subject
expect(response).to redirect_to(new_user_session_path)
end
end
describe 'GET #index' do
def make_request
get :index, params: { namespace_id: project.namespace, project_id: project }
get :index, params: project_params
end
it 'shows the page for users with guest role' do
sign_in(guest)
make_request
let(:user) { developer }
it 'shows the page' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
it 'shows the page for users with developer role' do
sign_in(developer)
make_request
context 'when user is unauthorized' do
let(:user) { anonymous }
it_behaves_like 'login required'
end
context 'when user is a guest' do
let(:user) { guest }
it 'shows the page' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
end
end
end
describe 'GET #show' do
def make_request
get :show, params: project_params(id: resource)
end
let_it_be(:resource) { create(:incident, project: project) }
let(:user) { developer }
it 'renders incident page' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
expect(response).to render_template(:show)
expect(assigns(:incident)).to be_present
expect(assigns(:incident).author.association(:status)).to be_loaded
expect(assigns(:issue)).to be_present
expect(assigns(:noteable)).to eq(assigns(:incident))
end
context 'when user is unauthorized' do
it 'redirects to the login page' do
make_request
context 'with feature flag disabled' do
before do
stub_feature_flags(issues_incident_details: false)
end
it_behaves_like 'not found'
end
context 'with non existing id' do
let(:resource) { non_existing_record_id }
it_behaves_like 'not found'
end
expect(response).to redirect_to(new_user_session_path)
context 'for issue' do
let_it_be(:resource) { create(:issue, project: project) }
it_behaves_like 'not found'
end
context 'when user is a guest' do
let(:user) { guest }
it 'shows the page' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
end
context 'when unauthorized' do
let(:user) { anonymous }
it_behaves_like 'login required'
end
end
private
def project_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, project_id: project)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Incident details', :js do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:incident) { create(:incident, project: project, author: developer) }
before_all do
project.add_developer(developer)
end
before do
sign_in(developer)
visit project_issues_incident_path(project, incident)
wait_for_requests
end
context 'when a developer+ displays the incident' do
it 'shows the incident' do
page.within('.issuable-details') do
expect(find('h2')).to have_content(incident.title)
end
end
it 'does not show design management' do
expect(page).not_to have_selector('.js-design-management')
end
end
end
......@@ -28,10 +28,10 @@ import mockFilters from '../mocks/incidents_filter.json';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
joinPaths: jest.fn().mockName('joinPaths'),
mergeUrlParams: jest.fn().mockName('mergeUrlParams'),
setUrlParams: jest.fn().mockName('setUrlParams'),
updateHistory: jest.fn().mockName('updateHistory'),
joinPaths: jest.fn(),
mergeUrlParams: jest.fn(),
setUrlParams: jest.fn(),
updateHistory: jest.fn(),
}));
describe('Incidents List', () => {
......@@ -81,12 +81,13 @@ describe('Incidents List', () => {
newIssuePath,
incidentTemplateName,
incidentType,
issuePath: '/project/isssues',
issuePath: '/project/issues',
publishedAvailable: true,
emptyListSvgPath,
textQuery: '',
authorUsernamesQuery: '',
assigneeUsernamesQuery: '',
issuesIncidentDetails: false,
},
stubs: {
GlButton: true,
......@@ -182,13 +183,6 @@ describe('Incidents List', () => {
expect(src).toBe(avatarUrl);
});
it('contains a link to the issue details', () => {
findTableRows()
.at(0)
.trigger('click');
expect(visitUrl).toHaveBeenCalledWith(joinPaths(`/project/isssues/`, mockIncidents[0].iid));
});
it('renders a closed icon for closed incidents', () => {
expect(findClosedIcon().length).toBe(
mockIncidents.filter(({ state }) => state === 'closed').length,
......@@ -199,6 +193,30 @@ describe('Incidents List', () => {
it('renders severity per row', () => {
expect(findSeverity().length).toBe(mockIncidents.length);
});
it('contains a link to the issue details page', () => {
findTableRows()
.at(0)
.trigger('click');
expect(visitUrl).toHaveBeenCalledWith(joinPaths(`/project/issues/`, mockIncidents[0].iid));
});
it('contains a link to the incident details page', async () => {
beforeEach(() =>
mountComponent({
data: { incidents: { list: mockIncidents }, incidentsCount: {} },
loading: false,
provide: { glFeatures: { issuesIncidentDetails: true } },
}),
);
findTableRows()
.at(0)
.trigger('click');
expect(visitUrl).toHaveBeenCalledWith(
joinPaths(`/project/issues/incident`, mockIncidents[0].iid),
);
});
});
describe('Create Incident', () => {
......@@ -218,11 +236,10 @@ describe('Incidents List', () => {
);
});
it('sets button loading on click', () => {
it('sets button loading on click', async () => {
findCreateIncidentBtn().vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(findCreateIncidentBtn().attributes('loading')).toBe('true');
});
await wrapper.vm.$nextTick();
expect(findCreateIncidentBtn().attributes('loading')).toBe('true');
});
it("doesn't show the button when list is empty", () => {
......@@ -254,51 +271,47 @@ describe('Incidents List', () => {
});
describe('prevPage', () => {
it('returns prevPage button', () => {
it('returns prevPage button', async () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(
findPagination()
.findAll('.page-item')
.at(0)
.text(),
).toBe('Prev');
});
await wrapper.vm.$nextTick();
expect(
findPagination()
.findAll('.page-item')
.at(0)
.text(),
).toBe('Prev');
});
it('returns prevPage number', () => {
it('returns prevPage number', async () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.prevPage).toBe(2);
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.prevPage).toBe(2);
});
it('returns 0 when it is the first page', () => {
it('returns 0 when it is the first page', async () => {
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.prevPage).toBe(0);
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.prevPage).toBe(0);
});
});
describe('nextPage', () => {
it('returns nextPage button', () => {
it('returns nextPage button', async () => {
findPagination().vm.$emit('input', 3);
return wrapper.vm.$nextTick(() => {
expect(
findPagination()
.findAll('.page-item')
.at(1)
.text(),
).toBe('Next');
});
await wrapper.vm.$nextTick();
expect(
findPagination()
.findAll('.page-item')
.at(1)
.text(),
).toBe('Next');
});
it('returns nextPage number', () => {
it('returns nextPage number', async () => {
mountComponent({
data: {
incidents: {
......@@ -312,17 +325,15 @@ describe('Incidents List', () => {
});
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBe(2);
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.nextPage).toBe(2);
});
it('returns `null` when currentPage is already last page', () => {
it('returns `null` when currentPage is already last page', async () => {
findStatusTabs().vm.$emit('input', 1);
findPagination().vm.$emit('input', 1);
return wrapper.vm.$nextTick(() => {
expect(wrapper.vm.nextPage).toBeNull();
});
await wrapper.vm.$nextTick();
expect(wrapper.vm.nextPage).toBeNull();
});
});
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe StartupjsHelper do
describe '#page_startup_graphql_calls' do
let(:query_location) { 'repository/queries/path_last_commit' }
let(:query_content) do
File.read(File.join(Rails.root, 'app/assets/javascripts', "#{query_location}.query.graphql"))
end
it 'returns an array containing GraphQL Page Startup Calls' do
helper.add_page_startup_graphql_call(query_location, { ref: 'foo' })
startup_graphql_calls = helper.page_startup_graphql_calls
expect(startup_graphql_calls).to include({ query: query_content, variables: { ref: 'foo' } })
end
end
end
......@@ -63,6 +63,17 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::OrderInfo do
expect(order_list.first.sort_direction).to eq :desc
end
end
context 'when ordering by CASE', :aggregate_failuers do
let(:relation) { Project.order(Arel::Nodes::Case.new(Project.arel_table[:pending_delete]).when(true).then(100).else(1000).asc) }
it 'assigns the right attribute name, named function, and direction' do
expect(order_list.count).to eq 1
expect(order_list.first.attribute_name).to eq 'pending_delete'
expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::Case)
expect(order_list.first.sort_direction).to eq :asc
end
end
end
describe '#validate_ordering' do
......
......@@ -53,16 +53,37 @@ RSpec.describe 'getting an issue list for a project' do
context 'when limiting the number of results' do
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
"issues(first: 1) { #{fields} }"
)
<<~GQL
query($path: ID!, $n: Int) {
project(fullPath: $path) {
issues(first: $n) { #{fields} }
}
}
GQL
end
let(:issue_limit) { 1 }
let(:variables) do
{ path: project.full_path, n: issue_limit }
end
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
post_graphql(query, current_user: current_user, variables: variables)
end
it 'only returns N issues' do
expect(issues_data.size).to eq(issue_limit)
end
end
context 'no limit is provided' do
let(:issue_limit) { nil }
it 'returns all issues' do
post_graphql(query, current_user: current_user, variables: variables)
expect(issues_data.size).to be > 1
end
end
......@@ -71,7 +92,7 @@ RSpec.describe 'getting an issue list for a project' do
# Newest first, we only want to see the newest checked
expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first)
post_graphql(query, current_user: current_user)
post_graphql(query, current_user: current_user, variables: variables)
end
end
......
......@@ -234,7 +234,8 @@ module GraphqlHelpers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {})
post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }, headers: headers
params = { query: query, variables: variables&.to_json }
post api('/', current_user, version: 'graphql'), params: params, headers: headers
end
def post_graphql_mutation(mutation, current_user: nil)
......
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