Commit 345bb505 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch...

Merge branch '335939-add-support-for-started-and-upcoming-milestone-filtering-to-issues-and-mr-rest-apis' into 'master'

Add support for issues filtering by milestone wildcards via REST APIs

See merge request gitlab-org/gitlab!69179
parents e59940bd c74db784
...@@ -7,7 +7,7 @@ module Types ...@@ -7,7 +7,7 @@ module Types
value 'NONE', 'No milestone is assigned.' value 'NONE', 'No milestone is assigned.'
value 'ANY', 'Milestone is assigned.' value 'ANY', 'Milestone is assigned.'
value 'STARTED', 'An open, started milestone (start date <= today).' value 'STARTED', 'Milestone assigned is open and started (start date <= today).'
value 'UPCOMING', 'An open milestone due in the future (due date >= today).' value 'UPCOMING', 'Milestone assigned is due closest in the future (due date > today).'
end end
end end
...@@ -5,7 +5,7 @@ module Types ...@@ -5,7 +5,7 @@ module Types
graphql_name 'NegatedMilestoneWildcardId' graphql_name 'NegatedMilestoneWildcardId'
description 'Negated Milestone ID wildcard values' description 'Negated Milestone ID wildcard values'
value 'STARTED', 'An open, started milestone (start date <= today).' value 'STARTED', 'Milestone assigned is open and yet to be started (start date > today).'
value 'UPCOMING', 'An open milestone due in the future (due date >= today).' value 'UPCOMING', 'Milestone assigned is open but due in the past (due date <= today).'
end end
end end
...@@ -15701,8 +15701,8 @@ Milestone ID wildcard values. ...@@ -15701,8 +15701,8 @@ Milestone ID wildcard values.
| ----- | ----------- | | ----- | ----------- |
| <a id="milestonewildcardidany"></a>`ANY` | Milestone is assigned. | | <a id="milestonewildcardidany"></a>`ANY` | Milestone is assigned. |
| <a id="milestonewildcardidnone"></a>`NONE` | No 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="milestonewildcardidstarted"></a>`STARTED` | Milestone assigned is open and started (start date <= today). |
| <a id="milestonewildcardidupcoming"></a>`UPCOMING` | An open milestone due in the future (due date >= today). | | <a id="milestonewildcardidupcoming"></a>`UPCOMING` | Milestone assigned is due closest in the future (due date > today). |
### `MoveType` ### `MoveType`
...@@ -15746,8 +15746,8 @@ Negated Milestone ID wildcard values. ...@@ -15746,8 +15746,8 @@ Negated Milestone ID wildcard values.
| Value | Description | | Value | Description |
| ----- | ----------- | | ----- | ----------- |
| <a id="negatedmilestonewildcardidstarted"></a>`STARTED` | An open, started milestone (start date <= today). | | <a id="negatedmilestonewildcardidstarted"></a>`STARTED` | Milestone assigned is open and yet to be started (start date > today). |
| <a id="negatedmilestonewildcardidupcoming"></a>`UPCOMING` | An open milestone due in the future (due date >= today). | | <a id="negatedmilestonewildcardidupcoming"></a>`UPCOMING` | Milestone assigned is open but due in the past (due date <= today). |
### `NetworkPolicyKind` ### `NetworkPolicyKind`
......
...@@ -68,10 +68,11 @@ GET /issues?state=opened ...@@ -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_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)_ | | `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. | | `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)_ | | `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))_ | | `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` | | `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)_ | | `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` | | `search` | string | no | Search issues against their `title` and `description` |
......
...@@ -43,9 +43,11 @@ module API ...@@ -43,9 +43,11 @@ module API
args.delete(:id) args.delete(:id)
args[:not] ||= {} args[:not] ||= {}
args[:milestone_title] ||= args.delete(:milestone) 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[: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[:scope] = args[:scope].underscore if args[:scope]
args[:sort] = "#{args[:order_by]}_#{args[:sort]}" args[:sort] = "#{args[:order_by]}_#{args[:sort]}"
args[:issue_types] ||= args.delete(:issue_type) args[:issue_types] ||= args.delete(:issue_type)
......
...@@ -14,6 +14,10 @@ module API ...@@ -14,6 +14,10 @@ module API
params :negatable_issue_filter_params do 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 :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, 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 :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' 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 ...@@ -32,9 +36,14 @@ module API
params :issues_stats_params do 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 :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, 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 :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 :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' 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_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' optional :author_username, type: String, desc: 'Return issues which are authored by the user with the given username'
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe API::Issues do RSpec.describe API::Issues do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) } 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(:project, reload: true) { create(:project, :public, :repository, creator_id: user.id, namespace: user.namespace) }
let_it_be(:private_mrs_project) do let_it_be(:private_mrs_project) do
...@@ -680,6 +682,71 @@ RSpec.describe API::Issues do ...@@ -680,6 +682,71 @@ RSpec.describe API::Issues do
end end
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 it 'returns an array of issues found by iids' do
get api('/issues', user), params: { iids: [closed_issue.iid] } 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