Commit c934e1b2 authored by charlie ablett's avatar charlie ablett

Merge branch 'swimlanes_collapse_mutation' into 'master'

Mutation to set board epic user preferences

See merge request gitlab-org/gitlab!42712
parents 871a23c1 7b3b5b0b
......@@ -11619,6 +11619,7 @@ type Mutation {
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload @deprecated(reason: "Use awardEmojiToggle. Deprecated in 13.2")
updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload
updateBoard(input: UpdateBoardInput!): UpdateBoardPayload
updateBoardEpicUserPreferences(input: UpdateBoardEpicUserPreferencesInput!): UpdateBoardEpicUserPreferencesPayload
updateBoardList(input: UpdateBoardListInput!): UpdateBoardListPayload
updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
......@@ -18470,6 +18471,51 @@ type UpdateAlertStatusPayload {
todo: Todo
}
"""
Autogenerated input type of UpdateBoardEpicUserPreferences
"""
input UpdateBoardEpicUserPreferencesInput {
"""
The board global ID
"""
boardId: BoardID!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Whether the epic should be collapsed in the board
"""
collapsed: Boolean!
"""
ID of an epic to set preferences for
"""
epicId: EpicID!
}
"""
Autogenerated return type of UpdateBoardEpicUserPreferences
"""
type UpdateBoardEpicUserPreferencesPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
User preferences for the epic in the board after mutation
"""
epicUserPreferences: BoardEpicUserPreferences
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Autogenerated input type of UpdateBoard
"""
......
......@@ -34109,6 +34109,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateBoardEpicUserPreferences",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateBoardEpicUserPreferencesInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateBoardEpicUserPreferencesPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateBoardList",
"description": null,
......@@ -54019,6 +54046,136 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateBoardEpicUserPreferencesInput",
"description": "Autogenerated input type of UpdateBoardEpicUserPreferences",
"fields": null,
"inputFields": [
{
"name": "boardId",
"description": "The board global ID",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "BoardID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "epicId",
"description": "ID of an epic to set preferences for",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "EpicID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "collapsed",
"description": "Whether the epic should be collapsed in the board",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UpdateBoardEpicUserPreferencesPayload",
"description": "Autogenerated return type of UpdateBoardEpicUserPreferences",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "epicUserPreferences",
"description": "User preferences for the epic in the board after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "BoardEpicUserPreferences",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateBoardInput",
......@@ -2623,6 +2623,16 @@ Autogenerated return type of UpdateAlertStatus.
| `issue` | Issue | The issue created after mutation |
| `todo` | Todo | The todo after mutation |
### UpdateBoardEpicUserPreferencesPayload
Autogenerated return type of UpdateBoardEpicUserPreferences.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `epicUserPreferences` | BoardEpicUserPreferences | User preferences for the epic in the board after mutation |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### UpdateBoardListPayload
Autogenerated return type of UpdateBoardList.
......
......@@ -28,6 +28,7 @@ module EE
mount_mutation ::Mutations::Vulnerabilities::RevertToDetected
mount_mutation ::Mutations::Boards::Update
mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics
mount_mutation ::Mutations::Boards::UpdateEpicUserPreferences
mount_mutation ::Mutations::InstanceSecurityDashboard::AddProject
mount_mutation ::Mutations::InstanceSecurityDashboard::RemoveProject
mount_mutation ::Mutations::DastOnDemandScans::Create
......
# frozen_string_literal: true
module Mutations
module Boards
class UpdateEpicUserPreferences < ::Mutations::BaseMutation
graphql_name 'UpdateBoardEpicUserPreferences'
argument :board_id,
::Types::GlobalIDType[::Board],
required: true,
description: 'The board global ID'
argument :epic_id,
::Types::GlobalIDType[::Epic],
required: true,
description: 'ID of an epic to set preferences for'
argument :collapsed,
GraphQL::BOOLEAN_TYPE,
required: true,
description: 'Whether the epic should be collapsed in the board'
field :epic_user_preferences,
Types::Boards::EpicUserPreferencesType,
null: true,
description: 'User preferences for the epic in the board after mutation'
authorize :read_board
def resolve(board_id:, epic_id:, **args)
board = authorized_find!(id: board_id)
raise_resource_not_available_error! unless epic = find_epic(epic_id)
result = ::Boards::EpicUserPreferences::UpdateService.new(
current_user, board, epic, { collapsed: args[:collapsed] }).execute
{
epic_user_preferences: result[:epic_user_preferences],
errors: result[:status] == :error ? [result[:message]] : []
}
end
private
def find_epic(epic_id)
epic = Epic.find(epic_id.model_id)
return unless Ability.allowed?(current_user, :read_epic, epic)
epic
rescue ActiveRecord::RecordNotFound
nil
end
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::Board)
end
end
end
end
......@@ -368,6 +368,11 @@ module EE
end
end
def find_or_init_board_epic_preference(board_id:, epic_id:)
boards_epic_user_preferences.find_or_initialize_by(
board_id: board_id, epic_id: epic_id)
end
protected
override :password_required?
......
# frozen_string_literal: true
module Boards
module EpicUserPreferences
class UpdateService < BaseService
def initialize(user, board, epic, preferences = {})
@current_user = user
@board = board
@epic = epic
@preferences = preferences
end
def execute
return error('User not set') unless current_user
preference = current_user.find_or_init_board_epic_preference(
board_id: board.id, epic_id: epic.id)
if preference.update(allowed_preferences)
success(epic_user_preferences: preference)
else
error(preference.errors.to_sentence)
end
end
private
attr_accessor :current_user, :board, :epic, :preferences
def allowed_preferences
preferences.slice(:collapsed)
end
end
end
end
---
title: Allow updating epic swimlanes collapsed status in GraphQL
merge_request: 42712
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Boards::UpdateEpicUserPreferences do
let_it_be(:group) { create(:group, :private) }
let_it_be(:project) { create(:project, :private) }
let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:epic) { create(:epic, group: group) }
let(:context) { { current_user: user } }
subject(:mutation) { described_class.new(object: nil, context: context, field: nil).resolve(mutation_params) }
describe '#resolve' do
before do
stub_licensed_features(epics: true)
end
let(:mutation_params) do
{
board_id: board.to_global_id,
epic_id: epic.to_global_id,
collapsed: true
}
end
it 'returns an error if the board is not accessible by the user' do
expect { mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when user can access the board' do
before do
project.add_developer(user)
end
it 'returns an error if the epic is not accessible by the user' do
expect { mutation }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when user can access the epic' do
before do
group.add_developer(user)
end
it 'returns updated preferences' do
expect(mutation[:errors]).to be_empty
expect(mutation[:epic_user_preferences].collapsed).to be_truthy
end
end
end
end
end
......@@ -1476,4 +1476,26 @@ RSpec.describe User do
expect(subject).to be false
end
end
describe '#find_or_init_board_epic_preference' do
let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board) }
let_it_be(:epic) { create(:epic) }
subject(:preference) { user.find_or_init_board_epic_preference(board_id: board.id, epic_id: epic.id) }
it 'returns new board epic user preference' do
expect(preference.persisted?).to be_falsey
expect(preference.user).to eq(user)
end
context 'when preference already exists' do
let_it_be(:epic_user_preference) { create(:epic_user_preference, board: board, epic: epic, user: user) }
it 'returns the existing board' do
expect(preference.persisted?).to be_truthy
expect(preference).to eq(epic_user_preference)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Update board epic user preferences' do
include GraphqlHelpers
let_it_be(:group) { create(:group, :private) }
let_it_be(:board) { create(:board, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:epic) { create(:epic, group: group) }
let(:mutation_class) { Mutations::Boards::UpdateEpicUserPreferences }
let(:mutation_name) { mutation_class.graphql_name }
let(:mutation_result_identifier) { mutation_name.camelize(:lower) }
let(:mutation) do
params = {
epic_id: epic.to_global_id.to_s,
board_id: board.to_global_id.to_s,
collapsed: true
}
graphql_mutation(mutation_name, params,
<<-QL.strip_heredoc
clientMutationId
epicUserPreferences {
collapsed
}
errors
QL
)
end
it 'returns an error if user can not access the board' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).to include(a_hash_including('message' => "The resource that you are attempting to access does " \
"not exist or you don't have permission to perform this action"))
end
context 'when user can access the board' do
before do
group.add_developer(user)
end
it 'returns an error if user can not access the epic' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).to include(a_hash_including('message' => "The resource that you are attempting to access does " \
"not exist or you don't have permission to perform this action"))
end
context 'when user can access the epic' do
before do
stub_licensed_features(epics: true)
end
it 'updates user preferences' do
post_graphql_mutation(mutation, current_user: user)
expect(response).to have_gitlab_http_status(:success)
preferences = json_response['data'][mutation_result_identifier]
expect(preferences['epicUserPreferences']['collapsed']).to be_truthy
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Boards::EpicUserPreferences::UpdateService, services: true do
describe '#execute' do
let_it_be(:group) { create(:group) }
let_it_be(:user) { create(:user) }
let_it_be(:board) { create(:board) }
let_it_be(:epic) { create(:epic, group: group) }
subject(:service) { described_class.new(user, board, epic, { collapsed: true }).execute }
it 'creates new preference' do
expect { service }.to change { Boards::EpicUserPreference.count }.by(1)
expect(service[:status]).to be_truthy
expect(service[:epic_user_preferences].collapsed).to be_truthy
end
context 'when user preference already exists' do
let_it_be(:epic_user_preference, reload: true) { create(:epic_user_preference, board: board, epic: epic, user: user) }
it 'updates existing preference' do
expect { service }.not_to change { Boards::EpicUserPreference.count }
expect(service[:status]).to be_truthy
expect(service[:epic_user_preferences].collapsed).to be_truthy
end
end
context 'when user is not set' do
let(:user) { nil }
it 'returns an error' do
expect(service[:status]).to eq(:error)
expect(service[:message]).to eq('User not set')
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment