diff --git a/app/graphql/mutations/user_preferences/update.rb b/app/graphql/mutations/user_preferences/update.rb index c92c6d725b7a4e3988b0c7da2a032bcb3aa0596d..b71c952b0f2e21fa59ddcb1a8f4855e87c401409 100644 --- a/app/graphql/mutations/user_preferences/update.rb +++ b/app/graphql/mutations/user_preferences/update.rb @@ -14,6 +14,15 @@ class Update < BaseMutation null: true, description: 'User preferences after mutation.' + def ready?(**args) + if disabled_sort_value?(args) + raise Gitlab::Graphql::Errors::ArgumentError, + 'Feature flag `incident_escalations` must be enabled to use this sort order.' + end + + super + end + def resolve(**attributes) user_preferences = current_user.user_preference user_preferences.update(attributes) @@ -23,6 +32,14 @@ def resolve(**attributes) errors: errors_on_object(user_preferences) } end + + private + + def disabled_sort_value?(args) + return false unless [:escalation_status_asc, :escalation_status_desc].include?(args[:issues_sort]) + + Feature.disabled?(:incident_escalations) + end end end end diff --git a/app/graphql/resolvers/base_issues_resolver.rb b/app/graphql/resolvers/base_issues_resolver.rb index 3e7509b4068aa5edafb0b832d53e4ddaa2fcfcbe..c5b228749de0d81aff6b44be9e66fb4b6c9ccb8b 100644 --- a/app/graphql/resolvers/base_issues_resolver.rb +++ b/app/graphql/resolvers/base_issues_resolver.rb @@ -17,7 +17,8 @@ class BaseIssuesResolver < BaseResolver NON_STABLE_CURSOR_SORTS = %i[priority_asc priority_desc popularity_asc popularity_desc label_priority_asc label_priority_desc - milestone_due_asc milestone_due_desc].freeze + milestone_due_asc milestone_due_desc + escalation_status_asc escalation_status_desc].freeze def continue_issue_resolve(parent, finder, **args) issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all { |q| apply_lookahead(q) } @@ -31,6 +32,13 @@ def continue_issue_resolve(parent, finder, **args) end end + def prepare_params(args, parent) + return unless [:escalation_status_asc, :escalation_status_desc].include?(args[:sort]) + return if Feature.enabled?(:incident_escalations, parent) + + args[:sort] = :created_desc # default for sort argument + end + private def unconditional_includes diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb index 38c79ff52acb5fdc508894758592782976931197..432d6f48607f4df75a814cfecd0817316ee979a9 100644 --- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb +++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb @@ -84,6 +84,7 @@ def resolve_with_lookahead(**args) prepare_assignee_username_params(args) prepare_release_tag_params(args) + prepare_params(args, parent) if defined?(prepare_params) finder = IssuesFinder.new(current_user, args) diff --git a/app/graphql/types/issue_sort_enum.rb b/app/graphql/types/issue_sort_enum.rb index f8825ff6c4616902a59bb862758a2eeef736ae91..db51e491d4e89d632f7a7a5e6e35a3e3294397a5 100644 --- a/app/graphql/types/issue_sort_enum.rb +++ b/app/graphql/types/issue_sort_enum.rb @@ -14,6 +14,8 @@ class IssueSortEnum < IssuableSortEnum value 'TITLE_DESC', 'Title by descending order.', value: :title_desc value 'POPULARITY_ASC', 'Number of upvotes (awarded "thumbs up" emoji) by ascending order.', value: :popularity_asc value 'POPULARITY_DESC', 'Number of upvotes (awarded "thumbs up" emoji) by descending order.', value: :popularity_desc + value 'ESCALATION_STATUS_ASC', 'Status from triggered to resolved. Defaults to `CREATED_DESC` if `incident_escalations` feature flag is disabled.', value: :escalation_status_asc + value 'ESCALATION_STATUS_DESC', 'Status from resolved to triggered. Defaults to `CREATED_DESC` if `incident_escalations` feature flag is disabled.', value: :escalation_status_desc end end diff --git a/app/models/issue.rb b/app/models/issue.rb index 75727fff2cddcd5f80644c27ed83c0a4cbf14f94..91d4b78f7c88f2ddc9b6f418317c676426061fc9 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -125,6 +125,8 @@ def most_recent scope :order_created_at_desc, -> { reorder(created_at: :desc) } scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') } scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') } + scope :order_escalation_status_asc, -> { includes(:incident_management_issuable_escalation_status).order(::Gitlab::Database.nulls_last_order('incident_management_issuable_escalation_status.status')) } + scope :order_escalation_status_desc, -> { includes(:incident_management_issuable_escalation_status).order(::Gitlab::Database.nulls_last_order('incident_management_issuable_escalation_status.status', 'DESC')) } scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) } scope :with_web_entity_associations, -> { preload(:author, project: [:project_feature, :route, namespace: :route]) } @@ -327,6 +329,8 @@ def self.sort_by_attribute(method, excluded_labels: []) when 'relative_position', 'relative_position_asc' then order_by_relative_position when 'severity_asc' then order_severity_asc.with_order_id_desc when 'severity_desc' then order_severity_desc.with_order_id_desc + when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc + when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc else super end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 434e0c43edff2a9946d0470fca329398c142003b..d1ab2cb0d794b35442becc6009b5c10752bf90c5 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -18298,6 +18298,8 @@ Values for sorting issues. | <a id="issuesortcreated_desc"></a>`CREATED_DESC` | Created at descending order. | | <a id="issuesortdue_date_asc"></a>`DUE_DATE_ASC` | Due date by ascending order. | | <a id="issuesortdue_date_desc"></a>`DUE_DATE_DESC` | Due date by descending order. | +| <a id="issuesortescalation_status_asc"></a>`ESCALATION_STATUS_ASC` | Status from triggered to resolved. Defaults to `CREATED_DESC` if `incident_escalations` feature flag is disabled. | +| <a id="issuesortescalation_status_desc"></a>`ESCALATION_STATUS_DESC` | Status from resolved to triggered. Defaults to `CREATED_DESC` if `incident_escalations` feature flag is disabled. | | <a id="issuesortlabel_priority_asc"></a>`LABEL_PRIORITY_ASC` | Label priority by ascending order. | | <a id="issuesortlabel_priority_desc"></a>`LABEL_PRIORITY_DESC` | Label priority by descending order. | | <a id="issuesortmilestone_due_asc"></a>`MILESTONE_DUE_ASC` | Milestone due date by ascending order. | diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 5e9a3d0a68b440e85ce50ddb98163c00f6ddca8f..81aeee0a3d24023c4bf8980b6d28399cf1b2457c 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -522,11 +522,53 @@ end end + context 'when sorting by escalation status' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:triggered_incident) { create(:incident, :with_escalation_status, project: project) } + let_it_be(:issue_no_status) { create(:issue, project: project) } + let_it_be(:resolved_incident) do + create(:incident, :with_escalation_status, project: project) + .tap { |issue| issue.escalation_status.resolve } + end + + it 'sorts issues ascending' do + issues = resolve_issues(sort: :escalation_status_asc).to_a + expect(issues).to eq([triggered_incident, resolved_incident, issue_no_status]) + end + + it 'sorts issues descending' do + issues = resolve_issues(sort: :escalation_status_desc).to_a + expect(issues).to eq([resolved_incident, triggered_incident, issue_no_status]) + end + + it 'sorts issues created_at' do + issues = resolve_issues(sort: :created_desc).to_a + expect(issues).to eq([resolved_incident, issue_no_status, triggered_incident]) + end + + context 'when incident_escalations feature flag is disabled' do + before do + stub_feature_flags(incident_escalations: false) + end + + it 'defaults ascending status sort to created_desc' do + issues = resolve_issues(sort: :escalation_status_asc).to_a + expect(issues).to eq([resolved_incident, issue_no_status, triggered_incident]) + end + + it 'defaults descending status sort to created_desc' do + issues = resolve_issues(sort: :escalation_status_desc).to_a + expect(issues).to eq([resolved_incident, issue_no_status, triggered_incident]) + end + end + end + context 'when sorting with non-stable cursors' do %i[priority_asc priority_desc popularity_asc popularity_desc label_priority_asc label_priority_desc - milestone_due_asc milestone_due_desc].each do |sort_by| + milestone_due_asc milestone_due_desc + escalation_status_asc escalation_status_desc].each do |sort_by| it "uses offset-pagination when sorting by #{sort_by}" do resolved = resolve_issues(sort: sort_by) diff --git a/spec/graphql/types/issue_sort_enum_spec.rb b/spec/graphql/types/issue_sort_enum_spec.rb index 4433709d193aed8668137093cdd958307eb29f33..95184477e7542d0c7b4bc2bf9f676065e94cb83a 100644 --- a/spec/graphql/types/issue_sort_enum_spec.rb +++ b/spec/graphql/types/issue_sort_enum_spec.rb @@ -9,7 +9,7 @@ it 'exposes all the existing issue sort values' do expect(described_class.values.keys).to include( - *%w[DUE_DATE_ASC DUE_DATE_DESC RELATIVE_POSITION_ASC SEVERITY_ASC SEVERITY_DESC] + *%w[DUE_DATE_ASC DUE_DATE_DESC RELATIVE_POSITION_ASC SEVERITY_ASC SEVERITY_DESC ESCALATION_STATUS_ASC ESCALATION_STATUS_DESC] ) end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 29305ba435c366e99a464ce795dc1b05e3993fd7..61ad9dc26bed4bed64fba41d82441f5c463286a3 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -238,6 +238,24 @@ end end + context 'order by escalation status' do + let_it_be(:triggered_incident) { create(:incident_management_issuable_escalation_status, :triggered).issue } + let_it_be(:resolved_incident) { create(:incident_management_issuable_escalation_status, :resolved).issue } + let_it_be(:issue_no_status) { create(:issue) } + + describe '.order_escalation_status_asc' do + subject { described_class.order_escalation_status_asc } + + it { is_expected.to eq([triggered_incident, resolved_incident, issue_no_status]) } + end + + describe '.order_escalation_status_desc' do + subject { described_class.order_escalation_status_desc } + + it { is_expected.to eq([resolved_incident, triggered_incident, issue_no_status]) } + end + end + # TODO: Remove when NOT NULL constraint is added to the relationship describe '#work_item_type' do let(:issue) { create(:issue, :incident, project: reusable_project, work_item_type: nil) } diff --git a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb index e1c7fd9d60dd55effb510985e3e52c53170a1e66..85194e6eb20b47b687bdee69e60da3fdf6a653f9 100644 --- a/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb +++ b/spec/requests/api/graphql/mutations/user_preferences/update_spec.rb @@ -28,6 +28,17 @@ expect(current_user.user_preference.persisted?).to eq(true) expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) end + + context 'when incident_escalations feature flag is disabled' do + let(:sort_value) { 'ESCALATION_STATUS_ASC' } + + before do + stub_feature_flags(incident_escalations: false) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['Feature flag `incident_escalations` must be enabled to use this sort order.'] + end end context 'when user has existing preference' do @@ -45,5 +56,16 @@ expect(current_user.user_preference.issues_sort).to eq(Types::IssueSortEnum.values[sort_value].value.to_s) end + + context 'when incident_escalations feature flag is disabled' do + let(:sort_value) { 'ESCALATION_STATUS_DESC' } + + before do + stub_feature_flags(incident_escalations: false) + end + + it_behaves_like 'a mutation that returns top-level errors', + errors: ['Feature flag `incident_escalations` must be enabled to use this sort order.'] + end end end