Commit 7b3b5b0b authored by Jan Provaznik's avatar Jan Provaznik Committed by charlie ablett

Add mutation to update epic user preferences

Board epic user preferences are used to store collapsed
status of the epic swimlane.
parent ff423740
...@@ -11650,6 +11650,7 @@ type Mutation { ...@@ -11650,6 +11650,7 @@ type Mutation {
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload @deprecated(reason: "Use awardEmojiToggle. Deprecated in 13.2") toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload @deprecated(reason: "Use awardEmojiToggle. Deprecated in 13.2")
updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload updateAlertStatus(input: UpdateAlertStatusInput!): UpdateAlertStatusPayload
updateBoard(input: UpdateBoardInput!): UpdateBoardPayload updateBoard(input: UpdateBoardInput!): UpdateBoardPayload
updateBoardEpicUserPreferences(input: UpdateBoardEpicUserPreferencesInput!): UpdateBoardEpicUserPreferencesPayload
updateBoardList(input: UpdateBoardListInput!): UpdateBoardListPayload updateBoardList(input: UpdateBoardListInput!): UpdateBoardListPayload
updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
...@@ -18581,6 +18582,51 @@ type UpdateAlertStatusPayload { ...@@ -18581,6 +18582,51 @@ type UpdateAlertStatusPayload {
todo: Todo 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 Autogenerated input type of UpdateBoard
""" """
......
...@@ -34180,6 +34180,33 @@ ...@@ -34180,6 +34180,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "updateBoardList",
"description": null, "description": null,
...@@ -54335,6 +54362,136 @@ ...@@ -54335,6 +54362,136 @@
"enumValues": null, "enumValues": null,
"possibleTypes": 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", "kind": "INPUT_OBJECT",
"name": "UpdateBoardInput", "name": "UpdateBoardInput",
...@@ -2638,6 +2638,16 @@ Autogenerated return type of UpdateAlertStatus. ...@@ -2638,6 +2638,16 @@ Autogenerated return type of UpdateAlertStatus.
| `issue` | Issue | The issue created after mutation | | `issue` | Issue | The issue created after mutation |
| `todo` | Todo | The todo 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 ### UpdateBoardListPayload
Autogenerated return type of UpdateBoardList. Autogenerated return type of UpdateBoardList.
......
...@@ -28,6 +28,7 @@ module EE ...@@ -28,6 +28,7 @@ module EE
mount_mutation ::Mutations::Vulnerabilities::RevertToDetected mount_mutation ::Mutations::Vulnerabilities::RevertToDetected
mount_mutation ::Mutations::Boards::Update mount_mutation ::Mutations::Boards::Update
mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics
mount_mutation ::Mutations::Boards::UpdateEpicUserPreferences
mount_mutation ::Mutations::InstanceSecurityDashboard::AddProject mount_mutation ::Mutations::InstanceSecurityDashboard::AddProject
mount_mutation ::Mutations::InstanceSecurityDashboard::RemoveProject mount_mutation ::Mutations::InstanceSecurityDashboard::RemoveProject
mount_mutation ::Mutations::DastOnDemandScans::Create 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
...@@ -379,6 +379,11 @@ module EE ...@@ -379,6 +379,11 @@ module EE
end end
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 protected
override :password_required? 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
...@@ -1446,4 +1446,26 @@ RSpec.describe User do ...@@ -1446,4 +1446,26 @@ RSpec.describe User do
expect(subject).to be false expect(subject).to be false
end end
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 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