From 08bac78bd67b7a44b606e3f07ed4ffbc4e6bb3e4 Mon Sep 17 00:00:00 2001 From: Mario Celi <mcelicalderon@gitlab.com> Date: Fri, 22 Jul 2022 16:11:07 -0500 Subject: [PATCH] Add startAndDueDate widget to workItemUpdate mutation Allows setting a start and due date to work items via the work item widget GraphQL API --- .../mutations/work_items/update_arguments.rb | 3 + .../start_and_due_date_update_input_type.rb | 18 ++++ .../concerns/work_items/widgetable_service.rb | 2 +- .../update_service.rb | 15 ++++ doc/api/graphql/reference/index.md | 11 +++ ...art_and_due_date_update_input_type_spec.rb | 9 ++ .../mutations/work_items/update_spec.rb | 88 +++++++++++++++++++ .../update_service_spec.rb | 62 +++++++++++++ 8 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb create mode 100644 app/services/work_items/widgets/start_and_due_date_service/update_service.rb create mode 100644 spec/graphql/types/work_items/widgets/start_and_due_date_update_input_type_spec.rb create mode 100644 spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb index a2670bd628f2c..66ec2c45cf8ed 100644 --- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb +++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb @@ -24,6 +24,9 @@ module UpdateArguments argument :hierarchy_widget, ::Types::WorkItems::Widgets::HierarchyUpdateInputType, required: false, description: 'Input for hierarchy widget.' + argument :start_and_due_date_widget, ::Types::WorkItems::Widgets::StartAndDueDateUpdateInputType, + required: false, + description: 'Input for start and due date widget.' end end end diff --git a/app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb b/app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb new file mode 100644 index 0000000000000..bccd4afe8f34e --- /dev/null +++ b/app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + class StartAndDueDateUpdateInputType < BaseInputObject + graphql_name 'WorkItemWidgetStartAndDueDateUpdateInput' + + argument :due_date, Types::DateType, + required: false, + description: 'Due date for the work item.' + argument :start_date, Types::DateType, + required: false, + description: 'Start date for the work item.' + end + end + end +end diff --git a/app/services/concerns/work_items/widgetable_service.rb b/app/services/concerns/work_items/widgetable_service.rb index 5665b07dce1ed..ffddf79cdf302 100644 --- a/app/services/concerns/work_items/widgetable_service.rb +++ b/app/services/concerns/work_items/widgetable_service.rb @@ -18,7 +18,7 @@ def widget_service(widget) # rubocop:enable Gitlab/ModuleWithInstanceVariables def widget_service_class(widget) - "WorkItems::Widgets::#{widget.type.capitalize}Service::#{self.class.name.demodulize}".constantize + "WorkItems::Widgets::#{widget.type.to_s.classify}Service::#{self.class.name.demodulize}".constantize rescue NameError nil end diff --git a/app/services/work_items/widgets/start_and_due_date_service/update_service.rb b/app/services/work_items/widgets/start_and_due_date_service/update_service.rb new file mode 100644 index 0000000000000..6a5dc0d5ef368 --- /dev/null +++ b/app/services/work_items/widgets/start_and_due_date_service/update_service.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module WorkItems + module Widgets + module StartAndDueDateService + class UpdateService < WorkItems::Widgets::BaseService + def before_update_callback(params: {}) + return if params.blank? + + widget.work_item.assign_attributes(params.slice(:start_date, :due_date)) + end + end + end + end +end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 00c329253e545..1a023a8aa4023 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5746,6 +5746,7 @@ Input type: `WorkItemUpdateInput` | <a id="mutationworkitemupdatedescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. | | <a id="mutationworkitemupdatehierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyUpdateInput`](#workitemwidgethierarchyupdateinput) | Input for hierarchy widget. | | <a id="mutationworkitemupdateid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. | +| <a id="mutationworkitemupdatestartandduedatewidget"></a>`startAndDueDateWidget` | [`WorkItemWidgetStartAndDueDateUpdateInput`](#workitemwidgetstartandduedateupdateinput) | Input for start and due date widget. | | <a id="mutationworkitemupdatestateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. | | <a id="mutationworkitemupdatetitle"></a>`title` | [`String`](#string) | Title of the work item. | | <a id="mutationworkitemupdateweightwidget"></a>`weightWidget` | [`WorkItemWidgetWeightInput`](#workitemwidgetweightinput) | Input for weight widget. | @@ -22465,6 +22466,7 @@ A time-frame defined as a closed inclusive range of two dates. | <a id="workitemupdatedtaskinputdescriptionwidget"></a>`descriptionWidget` | [`WorkItemWidgetDescriptionInput`](#workitemwidgetdescriptioninput) | Input for description widget. | | <a id="workitemupdatedtaskinputhierarchywidget"></a>`hierarchyWidget` | [`WorkItemWidgetHierarchyUpdateInput`](#workitemwidgethierarchyupdateinput) | Input for hierarchy widget. | | <a id="workitemupdatedtaskinputid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. | +| <a id="workitemupdatedtaskinputstartandduedatewidget"></a>`startAndDueDateWidget` | [`WorkItemWidgetStartAndDueDateUpdateInput`](#workitemwidgetstartandduedateupdateinput) | Input for start and due date widget. | | <a id="workitemupdatedtaskinputstateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. | | <a id="workitemupdatedtaskinputtitle"></a>`title` | [`String`](#string) | Title of the work item. | @@ -22493,6 +22495,15 @@ A time-frame defined as a closed inclusive range of two dates. | <a id="workitemwidgethierarchyupdateinputchildrenids"></a>`childrenIds` | [`[WorkItemID!]`](#workitemid) | Global IDs of children work items. | | <a id="workitemwidgethierarchyupdateinputparentid"></a>`parentId` | [`WorkItemID`](#workitemid) | Global ID of the parent work item. Use `null` to remove the association. | +### `WorkItemWidgetStartAndDueDateUpdateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="workitemwidgetstartandduedateupdateinputduedate"></a>`dueDate` | [`Date`](#date) | Due date for the work item. | +| <a id="workitemwidgetstartandduedateupdateinputstartdate"></a>`startDate` | [`Date`](#date) | Start date for the work item. | + ### `WorkItemWidgetWeightInput` #### Arguments diff --git a/spec/graphql/types/work_items/widgets/start_and_due_date_update_input_type_spec.rb b/spec/graphql/types/work_items/widgets/start_and_due_date_update_input_type_spec.rb new file mode 100644 index 0000000000000..91631093e4ea9 --- /dev/null +++ b/spec/graphql/types/work_items/widgets/start_and_due_date_update_input_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Types::WorkItems::Widgets::StartAndDueDateUpdateInputType do + it { expect(described_class.graphql_name).to eq('WorkItemWidgetStartAndDueDateUpdateInput') } + + it { expect(described_class.arguments.keys).to contain_exactly('startDate', 'dueDate') } +end diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb index 6ee3d5b1d3591..d5e1ec25a1383 100644 --- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -144,6 +144,94 @@ end end + context 'with due and start date widget input' do + let(:start_date) { Date.today } + let(:due_date) { 1.week.from_now.to_date } + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetStartAndDueDate { + startDate + dueDate + } + } + } + errors + FIELDS + end + + let(:input) do + { 'startAndDueDateWidget' => { 'startDate' => start_date.to_s, 'dueDate' => due_date.to_s } } + end + + it 'updates start and due date' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :start_date).from(nil).to(start_date).and( + change(work_item, :due_date).from(nil).to(due_date) + ) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'startDate' => start_date.to_s, + 'dueDate' => due_date.to_s, + 'type' => 'START_AND_DUE_DATE' + } + ) + end + + context 'when provided input is invalid' do + let(:due_date) { 1.week.ago.to_date } + + it 'returns validation errors without the work item' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to contain_exactly('Due date must be greater than or equal to start date') + end + end + + context 'when dates were already set for the work item' do + before do + work_item.update!(start_date: start_date, due_date: due_date) + end + + context 'when updating only start date' do + let(:input) do + { 'startAndDueDateWidget' => { 'startDate' => nil } } + end + + it 'allows setting a single date to null' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :start_date).from(start_date).to(nil).and( + not_change(work_item, :due_date).from(due_date) + ) + end + end + + context 'when updating only due date' do + let(:input) do + { 'startAndDueDateWidget' => { 'dueDate' => nil } } + end + + it 'allows setting a single date to null' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :due_date).from(due_date).to(nil).and( + not_change(work_item, :start_date).from(start_date) + ) + end + end + end + end + context 'with hierarchy widget input' do let(:widgets_response) { mutation_response['workItem']['widgets'] } let(:fields) do diff --git a/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb b/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb new file mode 100644 index 0000000000000..972a17c305226 --- /dev/null +++ b/spec/services/work_items/widgets/start_and_due_date_service/update_service_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::Widgets::StartAndDueDateService::UpdateService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:work_item) { create(:work_item, project: project) } + + let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::StartAndDueDate) } } + + describe '#before_update_callback' do + let(:start_date) { Date.today } + let(:due_date) { 1.week.from_now.to_date } + + subject(:update_params) do + described_class.new(widget: widget, current_user: user).before_update_callback(params: params) + end + + context 'when start and due date params are present' do + let(:params) { { start_date: Date.today, due_date: 1.week.from_now.to_date } } + + it 'correctly sets date values' do + expect do + update_params + end.to change(work_item, :start_date).from(nil).to(start_date).and( + change(work_item, :due_date).from(nil).to(due_date) + ) + end + end + + context 'when date params are not present' do + let(:params) { {} } + + it 'does not change work item date values' do + expect do + update_params + end.to not_change(work_item, :start_date).from(nil).and( + not_change(work_item, :due_date).from(nil) + ) + end + end + + context 'when work item had both date values already set' do + before do + work_item.update!(start_date: start_date, due_date: due_date) + end + + context 'when one of the two params is null' do + let(:params) { { start_date: nil } } + + it 'sets only one date to null' do + expect do + update_params + end.to change(work_item, :start_date).from(start_date).to(nil).and( + not_change(work_item, :due_date).from(due_date) + ) + end + end + end + end +end -- GitLab