diff --git a/app/models/work_item.rb b/app/models/work_item.rb index cd2de3fc335ab9ecb0d7be73de1477401ee8de4a..c4c991e5361cf9bbb46958a71e09da96d6e29b19 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -116,6 +116,17 @@ def related_link_class end end + def create_dates_source_from_current_dates + create_dates_source( + due_date: due_date, + start_date: start_date, + start_date_is_fixed: due_date.present? || start_date.present?, + due_date_is_fixed: due_date.present? || start_date.present?, + start_date_fixed: start_date, + due_date_fixed: due_date + ) + end + def noteable_target_type_name 'issue' end diff --git a/ee/app/services/work_items/widgets/rolledup_dates_service/hierarchies_update_service.rb b/ee/app/services/work_items/widgets/rolledup_dates_service/hierarchies_update_service.rb index 3697bbfceba7fb5b409e32d2cc9dcb1b5d06f5cd..3c23c14a8df20c37c74b4b86523ad6dc4dfbfcde 100644 --- a/ee/app/services/work_items/widgets/rolledup_dates_service/hierarchies_update_service.rb +++ b/ee/app/services/work_items/widgets/rolledup_dates_service/hierarchies_update_service.rb @@ -44,7 +44,7 @@ def execute def ensure_dates_sources_exist(work_items) work_items .excluding(work_items.joins(:dates_source)) # exclude work items that already have a dates source - .each(&:create_dates_source) + .each(&:create_dates_source_from_current_dates) end def join_with_update(query) diff --git a/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb index fc92081967a0ad098c57f4ee5de0cc4cdb3a5ac4..5959b2838d166f418430bac6bbd847b6830b7f30 100644 --- a/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb +++ b/ee/spec/requests/api/graphql/mutations/work_items/update_spec.rb @@ -1031,12 +1031,37 @@ def work_item_status context 'when updating parent' do let_it_be(:work_item_epic, reload: true) { create(:work_item, :epic_with_legacy_epic, namespace: group) } - let_it_be(:work_item_issue, reload: true) { create(:work_item, :issue, project: project) } + let_it_be(:work_item_issue, reload: true) do + create(:work_item, :issue, project: project, due_date: 2.days.from_now, start_date: 2.days.ago) + end let_it_be(:new_parent) { create(:work_item, :epic_with_legacy_epic, namespace: group) } let(:input) { { 'hierarchyWidget' => { 'parentId' => new_parent.to_global_id.to_s } } } + context 'when issue has due and start date set but no dates_source record' do + let(:input) { { 'hierarchyWidget' => { 'parentId' => work_item_epic.to_global_id.to_s } } } + + before do + allow(Gitlab::QueryLimiting::Transaction).to receive(:threshold).and_return(150) + end + + it 'does not clear due and start date values from issue', :sidekiq_inline, :aggregate_failures do + set_due_date = work_item_issue.due_date + set_start_date = work_item_issue.start_date + + expect(work_item_issue.dates_source).to be_nil + expect(set_due_date).to be_present + expect(set_start_date).to be_present + + expect do + post_graphql_mutation(mutation_for(work_item_issue), current_user: reporter) + end.to change { work_item_issue.reload.work_item_parent }.from(nil).to(work_item_epic) + .and not_change { work_item_issue.reload.due_date }.from(set_due_date) + .and not_change { work_item_issue.reload.start_date }.from(set_start_date) + end + end + it 'updates work item parent and synced epic parent when moving child is epic' do expect do post_graphql_mutation(mutation_for(work_item_epic), current_user: reporter) diff --git a/ee/spec/services/work_items/widgets/rolledup_dates_service/hierarchies_update_service_spec.rb b/ee/spec/services/work_items/widgets/rolledup_dates_service/hierarchies_update_service_spec.rb index 330a7bca4ce67d40eaa0e0561a382a1f83ab66cd..c69c2ba3ce4ea8dc6e3d0c89eba03fabd7ac4179 100644 --- a/ee/spec/services/work_items/widgets/rolledup_dates_service/hierarchies_update_service_spec.rb +++ b/ee/spec/services/work_items/widgets/rolledup_dates_service/hierarchies_update_service_spec.rb @@ -41,13 +41,9 @@ let_it_be_with_reload(:epic_1) { create(:epic, group: group, work_item: work_item_1) } let_it_be_with_reload(:epic_2) { create(:epic, group: group, work_item: work_item_2) } - subject(:service) do - described_class.new(WorkItem.id_in([ - work_item_1.id, - work_item_2.id, - work_item_fixed_dates.id - ])) - end + let(:work_items) { WorkItem.id_in([work_item_1.id, work_item_2.id, work_item_fixed_dates.id]) } + + subject(:service) { described_class.new(work_items) } shared_examples 'syncs work item dates sources to epics' do specify do @@ -234,4 +230,32 @@ .and not_change { work_item_fixed_dates.dates_source&.due_date } end end + + context 'when no dates_sources records exist' do + let_it_be(:project) { create(:project, group: group) } + let_it_be(:start_date) { Time.zone.today } + let_it_be(:due_date) { Time.zone.tomorrow } + + let_it_be_with_reload(:work_item_without_dates_source) do + create(:work_item, :issue, project: project, start_date: start_date, due_date: due_date).tap do |child| + create(:parent_link, work_item: child, work_item_parent: work_item_1) + end + end + + let(:work_items) do + WorkItem.id_in([work_item_1.id, work_item_2.id, work_item_fixed_dates.id, work_item_without_dates_source.id]) + end + + it 'ensures to create the records and sets correct default values' do + expect(work_item_without_dates_source.dates_source).to be_nil + + # We have multiple child work items that do not have a dates_source + expect { service.execute }.to change { WorkItems::DatesSource.count }.by(3) + + expect(work_item_without_dates_source.reload.dates_source).to have_attributes( + start_date: start_date, due_date: due_date, start_date_fixed: start_date, due_date_fixed: due_date, + start_date_is_fixed: true, due_date_is_fixed: true + ) + end + end end diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index 213ba7a19178f19d75c640f7cb096bdf17a6ede9..e3117b9bebc7cbf0235f736297497ec58c1484f2 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -92,6 +92,91 @@ end end + describe '#create_dates_source_from_current_dates' do + let_it_be(:start_date) { nil } + let_it_be(:due_date) { nil } + let(:dates_source) { work_item.dates_source } + + let_it_be_with_reload(:work_item) do + create(:work_item, project: reusable_project, start_date: start_date, due_date: due_date) + end + + before do + work_item.update!(start_date: start_date, due_date: due_date) + end + + subject(:create_dates_source_from_current_dates) { work_item.create_dates_source_from_current_dates } + + context 'when both due_date and start_date are present' do + let_it_be(:due_date) { Time.zone.tomorrow } + let_it_be(:start_date) { Time.zone.today } + + it 'creates dates_source with correct attributes' do + expect { create_dates_source_from_current_dates }.to change { WorkItems::DatesSource.count }.by(1) + + expect(dates_source.reload).to have_attributes( + due_date: due_date, + start_date: start_date, + start_date_is_fixed: true, + due_date_is_fixed: true, + start_date_fixed: start_date, + due_date_fixed: due_date + ) + end + end + + context 'when only due_date is present' do + let_it_be(:due_date) { Time.zone.tomorrow } + let_it_be(:start_date) { nil } + + it 'creates dates_source with correct attributes' do + create_dates_source_from_current_dates + + expect(dates_source).to have_attributes( + due_date: work_item.due_date, + start_date: nil, + start_date_is_fixed: true, + due_date_is_fixed: true, + start_date_fixed: nil, + due_date_fixed: work_item.due_date + ) + end + end + + context 'when only start_date is present' do + let_it_be(:due_date) { nil } + let_it_be(:start_date) { Time.zone.today } + + it 'creates dates_source with correct attributes' do + create_dates_source_from_current_dates + + expect(dates_source).to have_attributes( + due_date: nil, + start_date: work_item.start_date, + start_date_is_fixed: true, + due_date_is_fixed: true, + start_date_fixed: work_item.start_date, + due_date_fixed: nil + ) + end + end + + context 'when neither due_date nor start_date is present' do + it 'creates dates_source with correct attributes' do + dates_source = work_item.create_dates_source_from_current_dates + + expect(dates_source).to have_attributes( + due_date: nil, + start_date: nil, + start_date_is_fixed: false, + due_date_is_fixed: false, + start_date_fixed: nil, + due_date_fixed: nil + ) + end + end + end + describe '#noteable_target_type_name' do it 'returns `issue` as the target name' do work_item = build(:work_item)