Commit 771d9576 authored by Eulyeon Ko's avatar Eulyeon Ko

Allow sort and expose expired field for milestones

These changes are made to milestone-related GraphQL APIs.

- Milestone type gets a new 'expired' field indicating
whether milestone's due date is past the current date.

- When listing milstones, these arguments can be specified:

  - 'expiredLast' sorts milestones in layers:
  Current miletones are placed first, followed by
  milestones without due dates and expired milestones.
  Within layers, milestones are sorted by due date
  (asc by default) with ties being broken by id (desc).
  When 'expiredLast' is set, 'sort' argument other than
  due_date_desc or due_date_asc is ignored.

  - 'sort'

Changelog: added
parent 976dacef
...@@ -25,6 +25,15 @@ module Resolvers ...@@ -25,6 +25,15 @@ module Resolvers
required: false, required: false,
description: 'A date that the milestone contains.' description: 'A date that the milestone contains.'
argument :sort, Types::MilestoneSortEnum,
description: 'Sort milestones by this criteria.',
required: false,
default_value: :due_date_asc
argument :expired_last, GraphQL::BOOLEAN_TYPE,
description: 'Display non-expired milestones first when sorting milestones. In any sort the displayed order would be: non-expired milestones with due dates, non-expired milestones without due dates and expired milestones. Sort order other than due date is ignored.',
required: false
type Types::MilestoneType.connection_type, null: true type Types::MilestoneType.connection_type, null: true
def resolve(**args) def resolve(**args)
...@@ -32,7 +41,13 @@ module Resolvers ...@@ -32,7 +41,13 @@ module Resolvers
authorize! authorize!
MilestonesFinder.new(milestones_finder_params(args)).execute milestones = MilestonesFinder.new(milestones_finder_params(args)).execute
if args[:expired_last]
offset_pagination(milestones)
else
milestones
end
end end
private private
...@@ -43,6 +58,8 @@ module Resolvers ...@@ -43,6 +58,8 @@ module Resolvers
state: args[:state] || 'all', state: args[:state] || 'all',
title: args[:title], title: args[:title],
search_title: args[:search_title], search_title: args[:search_title],
sort: args[:sort],
expired_last: args[:expired_last],
containing_date: args[:containing_date] containing_date: args[:containing_date]
}.merge!(transform_timeframe_parameters(args)).merge!(parent_id_parameters(args)) }.merge!(transform_timeframe_parameters(args)).merge!(parent_id_parameters(args))
end end
......
# frozen_string_literal: true
module Types
class MilestoneSortEnum < SortEnum
graphql_name 'MilestoneSort'
description 'Values for sorting milestones'
value 'DUE_DATE_ASC', 'Milestone due date by ascending order.', value: :due_date_asc
value 'DUE_DATE_DESC', 'Milestone due date by descending order.', value: :due_date_desc
end
end
...@@ -26,6 +26,9 @@ module Types ...@@ -26,6 +26,9 @@ module Types
field :state, Types::MilestoneStateEnum, null: false, field :state, Types::MilestoneStateEnum, null: false,
description: 'State of the milestone.' description: 'State of the milestone.'
field :expired, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Expired state of the milestone (a milestone is expired when the due date is past the current date).'
field :web_path, GraphQL::STRING_TYPE, null: false, method: :milestone_path, field :web_path, GraphQL::STRING_TYPE, null: false, method: :milestone_path,
description: 'Web path of the milestone.' description: 'Web path of the milestone.'
......
...@@ -9564,10 +9564,12 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -9564,10 +9564,12 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="groupmilestonescontainingdate"></a>`containingDate` | [`Time`](#time) | A date that the milestone contains. | | <a id="groupmilestonescontainingdate"></a>`containingDate` | [`Time`](#time) | A date that the milestone contains. |
| <a id="groupmilestonesenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. | | <a id="groupmilestonesenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="groupmilestonesexpiredlast"></a>`expiredLast` | [`Boolean`](#boolean) | Display non-expired milestones first when sorting milestones. In any sort the displayed order would be: non-expired milestones with due dates, non-expired milestones without due dates and expired milestones. Sort order other than due date is ignored. |
| <a id="groupmilestonesids"></a>`ids` | [`[ID!]`](#id) | Array of global milestone IDs, e.g., `"gid://gitlab/Milestone/1"`. | | <a id="groupmilestonesids"></a>`ids` | [`[ID!]`](#id) | Array of global milestone IDs, e.g., `"gid://gitlab/Milestone/1"`. |
| <a id="groupmilestonesincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Include milestones from all parent groups. | | <a id="groupmilestonesincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Include milestones from all parent groups. |
| <a id="groupmilestonesincludedescendants"></a>`includeDescendants` | [`Boolean`](#boolean) | Include milestones from all subgroups and subprojects. | | <a id="groupmilestonesincludedescendants"></a>`includeDescendants` | [`Boolean`](#boolean) | Include milestones from all subgroups and subprojects. |
| <a id="groupmilestonessearchtitle"></a>`searchTitle` | [`String`](#string) | A search string for the title. | | <a id="groupmilestonessearchtitle"></a>`searchTitle` | [`String`](#string) | A search string for the title. |
| <a id="groupmilestonessort"></a>`sort` | [`MilestoneSort`](#milestonesort) | Sort milestones by this criteria. |
| <a id="groupmilestonesstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. | | <a id="groupmilestonesstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="groupmilestonesstate"></a>`state` | [`MilestoneStateEnum`](#milestonestateenum) | Filter milestones by state. | | <a id="groupmilestonesstate"></a>`state` | [`MilestoneStateEnum`](#milestonestateenum) | Filter milestones by state. |
| <a id="groupmilestonestimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. | | <a id="groupmilestonestimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
...@@ -10812,6 +10814,7 @@ Represents a milestone. ...@@ -10812,6 +10814,7 @@ Represents a milestone.
| <a id="milestonecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of milestone creation. | | <a id="milestonecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of milestone creation. |
| <a id="milestonedescription"></a>`description` | [`String`](#string) | Description of the milestone. | | <a id="milestonedescription"></a>`description` | [`String`](#string) | Description of the milestone. |
| <a id="milestoneduedate"></a>`dueDate` | [`Time`](#time) | Timestamp of the milestone due date. | | <a id="milestoneduedate"></a>`dueDate` | [`Time`](#time) | Timestamp of the milestone due date. |
| <a id="milestoneexpired"></a>`expired` | [`Boolean!`](#boolean) | Expired state of the milestone (a milestone is expired when the due date is past the current date). |
| <a id="milestonegroupmilestone"></a>`groupMilestone` | [`Boolean!`](#boolean) | Indicates if milestone is at group level. | | <a id="milestonegroupmilestone"></a>`groupMilestone` | [`Boolean!`](#boolean) | Indicates if milestone is at group level. |
| <a id="milestoneid"></a>`id` | [`ID!`](#id) | ID of the milestone. | | <a id="milestoneid"></a>`id` | [`ID!`](#id) | ID of the milestone. |
| <a id="milestoneiid"></a>`iid` | [`ID!`](#id) | Internal ID of the milestone. | | <a id="milestoneiid"></a>`iid` | [`ID!`](#id) | Internal ID of the milestone. |
...@@ -11889,9 +11892,11 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -11889,9 +11892,11 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="projectmilestonescontainingdate"></a>`containingDate` | [`Time`](#time) | A date that the milestone contains. | | <a id="projectmilestonescontainingdate"></a>`containingDate` | [`Time`](#time) | A date that the milestone contains. |
| <a id="projectmilestonesenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. | | <a id="projectmilestonesenddate"></a>`endDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.end. |
| <a id="projectmilestonesexpiredlast"></a>`expiredLast` | [`Boolean`](#boolean) | Display non-expired milestones first when sorting milestones. In any sort the displayed order would be: non-expired milestones with due dates, non-expired milestones without due dates and expired milestones. Sort order other than due date is ignored. |
| <a id="projectmilestonesids"></a>`ids` | [`[ID!]`](#id) | Array of global milestone IDs, e.g., `"gid://gitlab/Milestone/1"`. | | <a id="projectmilestonesids"></a>`ids` | [`[ID!]`](#id) | Array of global milestone IDs, e.g., `"gid://gitlab/Milestone/1"`. |
| <a id="projectmilestonesincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Also return milestones in the project's parent group and its ancestors. | | <a id="projectmilestonesincludeancestors"></a>`includeAncestors` | [`Boolean`](#boolean) | Also return milestones in the project's parent group and its ancestors. |
| <a id="projectmilestonessearchtitle"></a>`searchTitle` | [`String`](#string) | A search string for the title. | | <a id="projectmilestonessearchtitle"></a>`searchTitle` | [`String`](#string) | A search string for the title. |
| <a id="projectmilestonessort"></a>`sort` | [`MilestoneSort`](#milestonesort) | Sort milestones by this criteria. |
| <a id="projectmilestonesstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. | | <a id="projectmilestonesstartdate"></a>`startDate` **{warning-solid}** | [`Time`](#time) | **Deprecated** in 13.5. Use timeframe.start. |
| <a id="projectmilestonesstate"></a>`state` | [`MilestoneStateEnum`](#milestonestateenum) | Filter milestones by state. | | <a id="projectmilestonesstate"></a>`state` | [`MilestoneStateEnum`](#milestonestateenum) | Filter milestones by state. |
| <a id="projectmilestonestimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. | | <a id="projectmilestonestimeframe"></a>`timeframe` | [`Timeframe`](#timeframe) | List items overlapping the given timeframe. |
...@@ -14735,6 +14740,23 @@ Representation of whether a GitLab merge request can be merged. ...@@ -14735,6 +14740,23 @@ Representation of whether a GitLab merge request can be merged.
| <a id="mergestrategyenummerge_train"></a>`MERGE_TRAIN` | Use the merge_train merge strategy. | | <a id="mergestrategyenummerge_train"></a>`MERGE_TRAIN` | Use the merge_train merge strategy. |
| <a id="mergestrategyenummerge_when_pipeline_succeeds"></a>`MERGE_WHEN_PIPELINE_SUCCEEDS` | Use the merge_when_pipeline_succeeds merge strategy. | | <a id="mergestrategyenummerge_when_pipeline_succeeds"></a>`MERGE_WHEN_PIPELINE_SUCCEEDS` | Use the merge_when_pipeline_succeeds merge strategy. |
### `MilestoneSort`
Values for sorting milestones.
| Value | Description |
| ----- | ----------- |
| <a id="milestonesortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. |
| <a id="milestonesortcreated_desc"></a>`CREATED_DESC` | Created at descending order. |
| <a id="milestonesortdue_date_asc"></a>`DUE_DATE_ASC` | Milestone due date by ascending order. |
| <a id="milestonesortdue_date_desc"></a>`DUE_DATE_DESC` | Milestone due date by descending order. |
| <a id="milestonesortupdated_asc"></a>`UPDATED_ASC` | Updated at ascending order. |
| <a id="milestonesortupdated_desc"></a>`UPDATED_DESC` | Updated at descending order. |
| <a id="milestonesortcreated_asc"></a>`created_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_ASC`. |
| <a id="milestonesortcreated_desc"></a>`created_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `CREATED_DESC`. |
| <a id="milestonesortupdated_asc"></a>`updated_asc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_ASC`. |
| <a id="milestonesortupdated_desc"></a>`updated_desc` **{warning-solid}** | **Deprecated** in 13.5. This was renamed. Use: `UPDATED_DESC`. |
### `MilestoneStateEnum` ### `MilestoneStateEnum`
Current state of milestone. Current state of milestone.
......
...@@ -79,6 +79,32 @@ RSpec.describe Resolvers::GroupMilestonesResolver do ...@@ -79,6 +79,32 @@ RSpec.describe Resolvers::GroupMilestonesResolver do
end end
end end
context 'by sort' do
it 'calls MilestonesFinder with correct parameters' do
expect(MilestonesFinder).to receive(:new)
.with(args(group_ids: group.id, state: 'all', sort: :due_date_desc))
.and_call_original
resolve_group_milestones(sort: :due_date_desc)
end
end
context 'when expired_last is set' do
it 'calls MilestonesFinder with correct parameters' do
expect(MilestonesFinder).to receive(:new)
.with(args(group_ids: group.id, state: 'all', expired_last: true))
.and_call_original
resolve_group_milestones(expired_last: true)
end
it 'uses offset-pagination' do
resolved = resolve_group_milestones(expired_last: true)
expect(resolved).to be_a(::Gitlab::Graphql::Pagination::OffsetActiveRecordRelationConnection)
end
end
context 'by timeframe' do context 'by timeframe' do
context 'when start_date and end_date are present' do context 'when start_date and end_date are present' do
context 'when start date is after end_date' do context 'when start date is after end_date' do
......
...@@ -71,6 +71,26 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do ...@@ -71,6 +71,26 @@ RSpec.describe Resolvers::ProjectMilestonesResolver do
end end
end end
context 'by sort' do
it 'calls MilestonesFinder with correct parameters' do
expect(MilestonesFinder).to receive(:new)
.with(args(project_ids: project.id, state: 'all', sort: :due_date_desc))
.and_call_original
resolve_project_milestones(sort: :due_date_desc)
end
end
context 'when expired_last is set' do
it 'calls MilestonesFinder with correct parameters' do
expect(MilestonesFinder).to receive(:new)
.with(args(project_ids: project.id, state: 'all', expired_last: true))
.and_call_original
resolve_project_milestones(expired_last: true)
end
end
context 'by timeframe' do context 'by timeframe' do
context 'when start_date and end_date are present' do context 'when start_date and end_date are present' do
it 'calls MilestonesFinder with correct parameters' do it 'calls MilestonesFinder with correct parameters' do
......
...@@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['Milestone'] do ...@@ -9,7 +9,7 @@ RSpec.describe GitlabSchema.types['Milestone'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
id iid title description state web_path id iid title description state expired web_path
due_date start_date created_at updated_at due_date start_date created_at updated_at
project_milestone group_milestone subgroup_milestone project_milestone group_milestone subgroup_milestone
stats stats
......
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