diff --git a/app/graphql/mutations/todos/base_many.rb b/app/graphql/mutations/todos/base_many.rb index 8d424a5823dda6342449df930a270492e603b98f..eb87c90e2a48a72348eb0ade07342a40d834b6ad 100644 --- a/app/graphql/mutations/todos/base_many.rb +++ b/app/graphql/mutations/todos/base_many.rb @@ -16,11 +16,11 @@ class BaseMany < ::Mutations::BaseMutation # rubocop:disable GraphQL/GraphqlName null: false, description: 'Updated to-do items.' - def resolve(ids:) + def resolve(ids:, **kwargs) check_update_limit!(amount: ids.size) todos = authorized_find_all_pending_by_current_user(model_ids_of(ids)) - updated_ids = process_todos(todos) + updated_ids = process_todos(todos, **kwargs) { updated_ids: updated_ids, diff --git a/app/graphql/mutations/todos/snooze_many.rb b/app/graphql/mutations/todos/snooze_many.rb new file mode 100644 index 0000000000000000000000000000000000000000..13ea918ba5618c0b014259095d64e8ff4d9a637e --- /dev/null +++ b/app/graphql/mutations/todos/snooze_many.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Mutations + module Todos + class SnoozeMany < BaseMany + graphql_name 'TodoSnoozeMany' + + argument :snooze_until, + ::Types::TimeType, + required: true, + description: 'Time until which the todos should be snoozed.' + + private + + def process_todos(todos, snooze_until:) + ::Todos::SnoozingService.new.snooze_todos(todos, snooze_until) + end + + def todo_state_to_find + :pending + end + end + end +end diff --git a/app/graphql/mutations/todos/unsnooze_many.rb b/app/graphql/mutations/todos/unsnooze_many.rb new file mode 100644 index 0000000000000000000000000000000000000000..342ae41a8c292d0813de00f1f48dfd3cc3248ed9 --- /dev/null +++ b/app/graphql/mutations/todos/unsnooze_many.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Mutations + module Todos + class UnsnoozeMany < BaseMany + graphql_name 'TodoUnsnoozeMany' + + private + + def process_todos(todos) + ::Todos::SnoozingService.new.unsnooze_todos(todos) + end + + def todo_state_to_find + :pending + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index fe69314d34f8e6a272b4714d1b783a0898396bc9..e57974bc39c3ab2dc5012bdef03b3fc680df3ee4 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -149,6 +149,8 @@ class MutationType < BaseObject mount_mutation Mutations::Todos::RestoreMany mount_mutation Mutations::Todos::Snooze, experiment: { milestone: '17.4' } mount_mutation Mutations::Todos::UnSnooze, experiment: { milestone: '17.4' } + mount_mutation Mutations::Todos::SnoozeMany, experiment: { milestone: '17.9' } + mount_mutation Mutations::Todos::UnsnoozeMany, experiment: { milestone: '17.9' } mount_mutation Mutations::Snippets::Destroy mount_mutation Mutations::Snippets::Update mount_mutation Mutations::Snippets::Create diff --git a/app/services/todos/snoozing_service.rb b/app/services/todos/snoozing_service.rb index 106df80159c01323e7a34e6956d151def2375901..13242ea64e9bd0bcb766fb5787ca10cfd58d3cdd 100644 --- a/app/services/todos/snoozing_service.rb +++ b/app/services/todos/snoozing_service.rb @@ -5,7 +5,7 @@ # Used for snoozing/un-snoozing todos # # Ex. -# SnoozingService.new.snooze(todo, 1.day.from_now) +# Todos::SnoozingService.new.snooze_todo(todo, 1.day.from_now) # module Todos class SnoozingService @@ -24,5 +24,13 @@ def un_snooze_todo(todo) ServiceResponse.error(message: todo.errors.full_messages) end end + + def snooze_todos(todos, snooze_until) + todos.batch_update(snoozed_until: snooze_until) + end + + def unsnooze_todos(todos) + todos.batch_update(snoozed_until: nil) + end end end diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 38f1a4e58bcf8f683c1ac4069b4949ffedd65881..1f7ab374cc5ca599c6c1712cf51ebed50c39143b 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -10520,6 +10520,30 @@ Input type: `TodoSnoozeInput` | <a id="mutationtodosnoozeerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationtodosnoozetodo"></a>`todo` | [`Todo!`](#todo) | Requested to-do item. | +### `Mutation.todoSnoozeMany` + +DETAILS: +**Introduced** in GitLab 17.9. +**Status**: Experiment. + +Input type: `TodoSnoozeManyInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationtodosnoozemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationtodosnoozemanyids"></a>`ids` | [`[TodoID!]!`](#todoid) | Global IDs of the to-do items to process (a maximum of 100 is supported at once). | +| <a id="mutationtodosnoozemanysnoozeuntil"></a>`snoozeUntil` | [`Time!`](#time) | Time until which the todos should be snoozed. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationtodosnoozemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationtodosnoozemanyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationtodosnoozemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Updated to-do items. | + ### `Mutation.todoUnSnooze` DETAILS: @@ -10543,6 +10567,29 @@ Input type: `TodoUnSnoozeInput` | <a id="mutationtodounsnoozeerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationtodounsnoozetodo"></a>`todo` | [`Todo!`](#todo) | Requested to-do item. | +### `Mutation.todoUnsnoozeMany` + +DETAILS: +**Introduced** in GitLab 17.9. +**Status**: Experiment. + +Input type: `TodoUnsnoozeManyInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationtodounsnoozemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationtodounsnoozemanyids"></a>`ids` | [`[TodoID!]!`](#todoid) | Global IDs of the to-do items to process (a maximum of 100 is supported at once). | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationtodounsnoozemanyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationtodounsnoozemanyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationtodounsnoozemanytodos"></a>`todos` | [`[Todo!]!`](#todo) | Updated to-do items. | + ### `Mutation.todosMarkAllDone` Input type: `TodosMarkAllDoneInput` diff --git a/spec/graphql/mutations/todos/snooze_many_spec.rb b/spec/graphql/mutations/todos/snooze_many_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..dd58cab6a03c5cf70bc2a35f23991c16849d7f3b --- /dev/null +++ b/spec/graphql/mutations/todos/snooze_many_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Todos::SnoozeMany, feature_category: :notifications do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + let_it_be(:todo1) { create(:todo, user: user, author: other_user, state: :pending) } + let_it_be(:todo2) { create(:todo, user: user, author: other_user, state: :pending) } + let_it_be(:done_todo) { create(:todo, user: user, author: other_user, state: :done) } + let_it_be(:other_user_todo) { create(:todo, user: other_user, author: user, state: :pending) } + + let(:snooze_until) { 1.day.from_now } + let(:current_user) { user } + + describe '#process_todos' do + subject(:mutation) do + described_class + .new(object: nil, context: query_context, field: nil) + .resolve(ids: [ + global_id_of(todo1), + global_id_of(done_todo), + global_id_of(other_user_todo) + ], snooze_until: snooze_until) + end + + it 'snoozes current user\'s todos matching given ids until given timestamp' do + expect { mutation }.to change { todo1.reload.snoozed_until }.to be_within(1.second).of(snooze_until) + end + + it 'does not change other pending todos of the current user' do + expect { mutation }.not_to change { todo2.reload.snoozed_until } + end + + it 'does not change done todos' do + expect { mutation }.not_to change { done_todo.reload.snoozed_until } + end + + it 'does not change todos of other users' do + expect { mutation }.not_to change { other_user_todo.reload.snoozed_until } + end + + it 'returns the ids of processed todos' do + expect(mutation[:updated_ids]).to contain_exactly(todo1.id) + end + end +end diff --git a/spec/graphql/mutations/todos/unsnooze_many_spec.rb b/spec/graphql/mutations/todos/unsnooze_many_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f265cc09bdf6906588b2ef52b5e6a398ff790ed9 --- /dev/null +++ b/spec/graphql/mutations/todos/unsnooze_many_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Todos::UnsnoozeMany, feature_category: :notifications do + include GraphqlHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:other_user) { create(:user) } + let_it_be(:snoozed_todo1) { create(:todo, user: user, author: other_user, snoozed_until: 8.hours.from_now) } + let_it_be(:snoozed_todo2) { create(:todo, user: user, author: other_user, snoozed_until: 5.days.from_now) } + let_it_be(:done_todo) { create(:todo, user: user, author: other_user, state: :done) } + let_it_be(:other_user_snoozed_todo) { create(:todo, user: other_user, author: user, snoozed_until: 1.hour.from_now) } + + let(:current_user) { user } + + describe '#process_todos' do + subject(:mutation) do + described_class + .new(object: nil, context: query_context, field: nil) + .resolve(ids: [ + global_id_of(snoozed_todo1), + global_id_of(done_todo), + global_id_of(other_user_snoozed_todo) + ]) + end + + it 'unsnoozes current user\'s todos matching given ids until given timestamp' do + expect { mutation }.to change { snoozed_todo1.reload.snoozed_until }.to be_nil + end + + it 'does not change other snoozed todos of the current user' do + expect { mutation }.not_to change { snoozed_todo2.reload.snoozed_until } + end + + it 'does not change done todos' do + expect { mutation }.not_to change { done_todo.reload.snoozed_until } + end + + it 'does not change todos of other users' do + expect { mutation }.not_to change { other_user_snoozed_todo.reload.snoozed_until } + end + + it 'returns the ids of processed todos' do + expect(mutation[:updated_ids]).to contain_exactly(snoozed_todo1.id) + end + end +end diff --git a/spec/services/todos/snoozing_service_spec.rb b/spec/services/todos/snoozing_service_spec.rb index 26a3e364eddd46f70d33382c402282095a20e754..bc5a48a0de376b3243fe9e4b61db0692b675abed 100644 --- a/spec/services/todos/snoozing_service_spec.rb +++ b/spec/services/todos/snoozing_service_spec.rb @@ -83,4 +83,43 @@ end end end + + describe '#snooze_todos' do + let_it_be(:time) { 8.hours.from_now } + let_it_be(:todo1) { create(:todo, :pending, user: user) } + let_it_be(:todo2) { create(:todo, :pending, user: user, snoozed_until: 1.hour.ago) } + let(:todos) { Todo.where(id: [todo1.id, todo2.id]) } + + it 'snoozes all todos until the provided time' do + service.snooze_todos(todos, time) + + expect(todo1.reload.snoozed_until).to be_within(1.second).of(time) + expect(todo2.reload.snoozed_until).to be_within(1.second).of(time) + end + + it 'responds with the updated todo ids' do + response = service.snooze_todos(todos, time) + + expect(response).to match_array [todo1.id, todo2.id] + end + end + + describe '#unsnooze_todos' do + let_it_be(:todo1) { create(:todo, :pending, user: user, snoozed_until: 1.day.from_now) } + let_it_be(:todo2) { create(:todo, :pending, user: user, snoozed_until: nil) } + let(:todos) { Todo.where(id: [todo1.id, todo2.id]) } + + it 'unsnoozes all todos' do + service.unsnooze_todos(todos) + + expect(todo1.reload.snoozed_until).to be_nil + expect(todo2.reload.snoozed_until).to be_nil + end + + it 'responds with the updated todo ids' do + response = service.unsnooze_todos(todos) + + expect(response).to match_array [todo1.id, todo2.id] + end + end end