Commit c74db784 authored by Eulyeon Ko's avatar Eulyeon Ko Committed by Jan Provaznik

Add support for issues filtering by milestone wildcards via REST APIs

parent 36848426
......@@ -7,7 +7,7 @@ module Types
value 'NONE', 'No milestone is assigned.'
value 'ANY', 'Milestone is assigned.'
value 'STARTED', 'An open, started milestone (start date <= today).'
value 'UPCOMING', 'An open milestone due in the future (due date >= today).'
value 'STARTED', 'Milestone assigned is open and started (start date <= today).'
value 'UPCOMING', 'Milestone assigned is due closest in the future (due date > today).'
end
end
......@@ -5,7 +5,7 @@ module Types
graphql_name 'NegatedMilestoneWildcardId'
description 'Negated Milestone ID wildcard values'
value 'STARTED', 'An open, started milestone (start date <= today).'
value 'UPCOMING', 'An open milestone due in the future (due date >= today).'
value 'STARTED', 'Milestone assigned is open and yet to be started (start date > today).'
value 'UPCOMING', 'Milestone assigned is open but due in the past (due date <= today).'
end
end
......@@ -15664,8 +15664,8 @@ Milestone ID wildcard values.
| ----- | ----------- |
| <a id="milestonewildcardidany"></a>`ANY` | Milestone is assigned. |
| <a id="milestonewildcardidnone"></a>`NONE` | No milestone is assigned. |
| <a id="milestonewildcardidstarted"></a>`STARTED` | An open, started milestone (start date <= today). |
| <a id="milestonewildcardidupcoming"></a>`UPCOMING` | An open milestone due in the future (due date >= today). |
| <a id="milestonewildcardidstarted"></a>`STARTED` | Milestone assigned is open and started (start date <= today). |
| <a id="milestonewildcardidupcoming"></a>`UPCOMING` | Milestone assigned is due closest in the future (due date > today). |
### `MoveType`
......@@ -15709,8 +15709,8 @@ Negated Milestone ID wildcard values.
| Value | Description |
| ----- | ----------- |
| <a id="negatedmilestonewildcardidstarted"></a>`STARTED` | An open, started milestone (start date <= today). |
| <a id="negatedmilestonewildcardidupcoming"></a>`UPCOMING` | An open milestone due in the future (due date >= today). |
| <a id="negatedmilestonewildcardidstarted"></a>`STARTED` | Milestone assigned is open and yet to be started (start date > today). |
| <a id="negatedmilestonewildcardidupcoming"></a>`UPCOMING` | Milestone assigned is open but due in the past (due date <= today). |
### `NetworkPolicyKind`
......
......@@ -68,10 +68,11 @@ GET /issues?state=opened
| `iteration_id` **(PREMIUM)** | integer | no | Return issues assigned to the given iteration ID. `None` returns issues that do not belong to an iteration. `Any` returns issues that belong to an iteration. Mutually exclusive with `iteration_title`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6)_ |
| `iteration_title` **(PREMIUM)** | string | no | Return issues assigned to the iteration with the given title. Similar to `iteration_id` and mutually exclusive with `iteration_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6)_ |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. Using `None` or `Any` will be [deprecated in the future](https://gitlab.com/gitlab-org/gitlab/-/issues/336044). Please use `milestone_id` attribute instead. `milestone` and `milestone_id` are mutually exclusive. |
| `milestone_id` | string | no | Returns issues assigned to milestones with a given timebox value (`None`, `Any`, `Upcoming`, and `Started`). `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. `Upcoming` lists all issues assigned to milestones due in the future. `Started` lists all issues assigned to open, started milestones. `milestone` and `milestone_id` are mutually exclusive. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335939) in GitLab 14.3)_ |
| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/14016) in GitLab 10.0)_ |
| `non_archived` | boolean | no | Return issues only from non-archived projects. If `false`, the response returns issues from both archived and non-archived projects. Default is `true`. _(Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/197170))_ |
| `not` | Hash | no | Return issues that do not match the parameters supplied. Accepts: `assignee_id`, `assignee_username`, `author_id`, `author_username`, `iids`, `iteration_id`, `iteration_title`, `labels`, `milestone`, and `weight`. |
| `not` | Hash | no | Return issues that do not match the parameters supplied. Accepts: `assignee_id`, `assignee_username`, `author_id`, `author_username`, `iids`, `iteration_id`, `iteration_title`, `labels`, `milestone`, `milestone_id` and `weight`. |
| `order_by` | string | no | Return issues ordered by `created_at`, `updated_at`, `priority`, `due_date`, `relative_position`, `label_priority`, `milestone_due`, `popularity`, `weight` fields. Default is `created_at` |
| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13004) in GitLab 9.5. [Changed to snake_case](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/18935) in GitLab 11.0)_ |
| `search` | string | no | Search issues against their `title` and `description` |
......
......@@ -43,9 +43,11 @@ module API
args.delete(:id)
args[:not] ||= {}
args[:milestone_title] ||= args.delete(:milestone)
args[:not][:milestone_title] ||= args[:not]&.delete(:milestone)
args[:milestone_wildcard_id] ||= args.delete(:milestone_id)
args[:not][:milestone_title] ||= args[:not].delete(:milestone)
args[:not][:milestone_wildcard_id] ||= args[:not].delete(:milestone_id)
args[:label_name] ||= args.delete(:labels)
args[:not][:label_name] ||= args[:not]&.delete(:labels)
args[:not][:label_name] ||= args[:not].delete(:labels)
args[:scope] = args[:scope].underscore if args[:scope]
args[:sort] = "#{args[:order_by]}_#{args[:sort]}"
args[:issue_types] ||= args.delete(:issue_type)
......
......@@ -14,6 +14,10 @@ module API
params :negatable_issue_filter_params do
optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
optional :milestone_id, types: String, values: %w[Any None Upcoming Started],
desc: 'Return issues assigned to milestones without the specified timebox value ("Any", "None", "Upcoming" or "Started")'
mutually_exclusive :milestone_id, :milestone
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues'
optional :author_id, type: Integer, desc: 'Return issues which are not authored by the user with the given ID'
......@@ -32,9 +36,14 @@ module API
params :issues_stats_params do
optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
# 'milestone_id' only accepts wildcard values 'Any', 'None', 'Upcoming', 'Started'
# the param has '_id' in the name to keep consistency (ex. assignee_id accepts id and wildcard values).
optional :milestone_id, types: String, values: %w[Any None Upcoming Started],
desc: 'Return issues assigned to milestones with the specified timebox value ("Any", "None", "Upcoming" or "Started")'
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues'
optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these'
optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma'
mutually_exclusive :milestone_id, :milestone
optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
optional :author_username, type: String, desc: 'Return issues which are authored by the user with the given username'
......
......@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe API::Issues do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
let_it_be(:private_mrs_project) do
......@@ -680,6 +682,71 @@ RSpec.describe API::Issues do
end
end
context 'filtering by milestone_id' do
let_it_be(:upcoming_milestone) { create(:milestone, project: project, title: "upcoming milestone", start_date: 1.day.ago, due_date: 1.day.from_now) }
let_it_be(:started_milestone) { create(:milestone, project: project, title: "started milestone", start_date: 2.days.ago, due_date: 1.day.ago) }
let_it_be(:future_milestone) { create(:milestone, project: project, title: "future milestone", start_date: 7.days.from_now, due_date: 14.days.from_now) }
let_it_be(:issue_upcoming) { create(:issue, project: project, state: :opened, milestone: upcoming_milestone) }
let_it_be(:issue_started) { create(:issue, project: project, state: :opened, milestone: started_milestone) }
let_it_be(:issue_future) { create(:issue, project: project, state: :opened, milestone: future_milestone) }
let_it_be(:issue_none) { create(:issue, project: project, state: :opened) }
let(:wildcard_started) { 'Started' }
let(:wildcard_upcoming) { 'Upcoming' }
let(:wildcard_any) { 'Any' }
let(:wildcard_none) { 'None' }
where(:milestone_id, :not_milestone, :expected_issues) do
ref(:wildcard_none) | nil | lazy { [issue_none.id] }
ref(:wildcard_any) | nil | lazy { [issue_future.id, issue_started.id, issue_upcoming.id, issue.id, closed_issue.id] }
ref(:wildcard_started) | nil | lazy { [issue_started.id, issue_upcoming.id] }
ref(:wildcard_upcoming) | nil | lazy { [issue_upcoming.id] }
ref(:wildcard_any) | "upcoming milestone" | lazy { [issue_future.id, issue_started.id, issue.id, closed_issue.id] }
ref(:wildcard_upcoming) | "upcoming milestone" | []
end
with_them do
it "returns correct issues when filtering with 'milestone_id' and optionally negated 'milestone'" do
get api('/issues', user), params: { milestone_id: milestone_id, not: not_milestone ? { milestone: not_milestone } : {} }
expect_paginated_array_response(expected_issues)
end
end
context 'negated filtering' do
where(:not_milestone_id, :expected_issues) do
ref(:wildcard_started) | lazy { [issue_future.id] }
ref(:wildcard_upcoming) | lazy { [issue_started.id] }
end
with_them do
it "returns correct issues when filtering with negated 'milestone_id'" do
get api('/issues', user), params: { not: { milestone_id: not_milestone_id } }
expect_paginated_array_response(expected_issues)
end
end
end
context 'when mutually exclusive params are passed' do
where(:params) do
[
[lazy { { milestone: "foo", milestone_id: wildcard_any } }],
[lazy { { not: { milestone: "foo", milestone_id: wildcard_any } } }]
]
end
with_them do
it "raises an error", :aggregate_failures do
get api('/issues', user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response["error"]).to include("mutually exclusive")
end
end
end
end
it 'returns an array of issues found by iids' do
get api('/issues', user), params: { iids: [closed_issue.iid] }
......
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