From 6c346448fd3e3443e04d96b90b9bdf9df2be1a3e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 8 Jul 2022 15:08:58 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .browserslistrc | 15 ++-- .gitlab/ci/global.gitlab-ci.yml | 2 +- app/assets/stylesheets/pages/issuable.scss | 2 +- .../stylesheets/startup/startup-dark.scss | 1 - .../stylesheets/startup/startup-general.scss | 1 - app/assets/stylesheets/utilities.scss | 4 + .../admin/broadcast_messages_controller.rb | 2 +- app/finders/ci/runners_finder.rb | 8 ++ .../mutations/work_items/widgetable.rb | 19 +++++ app/graphql/mutations/work_items/create.rb | 18 +++- app/graphql/mutations/work_items/update.rb | 13 +-- app/graphql/resolvers/ci/runners_resolver.rb | 5 ++ .../widgets/hierarchy_create_input_type.rb | 16 ++++ app/helpers/nav/new_dropdown_helper.rb | 2 +- app/models/ci/runner.rb | 5 ++ app/models/work_item.rb | 8 +- .../concerns/work_items/widgetable_service.rb | 26 ++++++ app/services/issuable_base_service.rb | 6 +- app/services/work_items/create_service.rb | 20 +++-- app/services/work_items/update_service.rb | 23 +---- .../widgets/hierarchy_service/base_service.rb | 32 +++++++ .../hierarchy_service/create_service.rb | 17 ++++ .../hierarchy_service/update_service.rb | 34 -------- .../member_access_denied_email.html.haml | 11 +-- db/docs/broadcast_messages.yml | 4 +- doc/api/graphql/reference/index.md | 11 +++ lib/api/broadcast_messages.rb | 2 +- lib/gitlab/ci/runner_releases.rb | 58 ++++++++++--- lib/gitlab/jira_import/issue_serializer.rb | 6 +- lib/gitlab/jira_import/issues_importer.rb | 10 ++- locale/gitlab.pot | 15 ++++ spec/features/nav/top_nav_responsive_spec.rb | 2 +- spec/features/nav/top_nav_tooltip_spec.rb | 4 +- spec/finders/ci/runners_finder_spec.rb | 61 +++++++++++++ .../resolvers/ci/runners_resolver_spec.rb | 2 + spec/helpers/nav/new_dropdown_helper_spec.rb | 2 +- spec/lib/gitlab/ci/runner_releases_spec.rb | 77 ++++++++++++++--- .../jira_import/issue_serializer_spec.rb | 6 +- .../jira_import/issues_importer_spec.rb | 19 ++++- spec/models/ci/runner_spec.rb | 35 ++++++++ spec/requests/api/api_spec.rb | 4 +- .../mutations/work_items/create_spec.rb | 64 ++++++++++++++ .../work_items/create_service_spec.rb | 85 ++++++++++++++++++- .../work_items/update_service_spec.rb | 30 +++++++ .../widgetable_service_shared_examples.rb | 13 +++ 45 files changed, 655 insertions(+), 145 deletions(-) create mode 100644 app/graphql/mutations/concerns/mutations/work_items/widgetable.rb create mode 100644 app/graphql/types/work_items/widgets/hierarchy_create_input_type.rb create mode 100644 app/services/concerns/work_items/widgetable_service.rb create mode 100644 app/services/work_items/widgets/hierarchy_service/create_service.rb create mode 100644 spec/support/shared_examples/work_items/widgetable_service_shared_examples.rb diff --git a/.browserslistrc b/.browserslistrc index a608ac7c734..1bddd91cf0d 100644 --- a/.browserslistrc +++ b/.browserslistrc @@ -3,14 +3,13 @@ # https://docs.gitlab.com/ee/install/requirements.html#supported-web-browsers # with the following reasoning: # -# - We should support the latest ESR of Firefox: 78, because it used quite a lot. -# - We use Edge/Chrome >= 84 because 83 had an annoying bug which would mean we -# need to polyfill Array.reduce: https://bugs.chromium.org/p/chromium/issues/detail?id=1049982 -# - Safari 13.1 because it is the current minor version of the previous major version +# - We should support the latest ESR of Firefox: 91, because it used quite a lot. +# - We use Edge/Chrome >= 92 because they are about as old as the Firefox ESR +# - Safari 14.1 because it is the current minor version of the previous major version # # See also this epic: https://gitlab.com/groups/gitlab-org/-/epics/3957 # -chrome >= 84 -edge >= 84 -firefox >= 78 -safari >= 13.1 +chrome >= 92 +edge >= 92 +firefox >= 91 +safari >= 14.1 \ No newline at end of file diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 344a31b28d8..ff941c47563 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -79,7 +79,7 @@ policy: push # We want to rebuild the cache from scratch to ensure stale dependencies are cleaned up. .assets-cache: &assets-cache - key: "assets-debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}-node-${NODE_ENV}" + key: "assets-debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}-node-${NODE_ENV}-v2" paths: - assets-hash.txt - public/assets/webpack/ diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 645e1449a1c..cedb2522c4b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -296,7 +296,7 @@ @include media-breakpoint-up(lg) { padding: 0; - form { + .issuable-context-form { --initial-top: calc(#{$header-height} + #{$mr-tabs-height}); --top: var(--initial-top); diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 62205ea4466..801c9ea828f 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -2056,7 +2056,6 @@ body.gl-dark { --nav-active-bg: rgba(255, 255, 255, 0.08); } .tab-width-8 { - -moz-tab-size: 8; tab-size: 8; } .gl-sr-only { diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index b4f333fceab..43ca5a512d5 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -1698,7 +1698,6 @@ svg.s16 { } .tab-width-8 { - -moz-tab-size: 8; tab-size: 8; } .gl-sr-only { diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index d7a5e21e303..6bd05f90f26 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -366,3 +366,7 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709 /* stylelint-disable property-no-vendor-prefix */ -webkit-backdrop-filter: blur(2px); // still required by Safari } + +.gl-flex-flow-row-wrap { + flex-flow: row wrap; +} diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index 865af244773..bf573d45852 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -5,7 +5,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController before_action :finder, only: [:edit, :update, :destroy] - feature_category :navigation + feature_category :onboarding urgency :low # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb index 356915722fe..4f9244d9825 100644 --- a/app/finders/ci/runners_finder.rb +++ b/app/finders/ci/runners_finder.rb @@ -17,6 +17,7 @@ module Ci search! filter_by_active! filter_by_status! + filter_by_upgrade_status! filter_by_runner_type! filter_by_tag_list! sort! @@ -67,6 +68,13 @@ module Ci filter_by!(:status_status, Ci::Runner::AVAILABLE_STATUSES) end + def filter_by_upgrade_status! + return unless @params.key?(:upgrade_status) + return unless Ci::RunnerVersion.statuses.key?(@params[:upgrade_status]) + + @runners = @runners.with_upgrade_status(@params[:upgrade_status]) + end + def filter_by_runner_type! filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES) end diff --git a/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb b/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb new file mode 100644 index 00000000000..22f782514c9 --- /dev/null +++ b/app/graphql/mutations/concerns/mutations/work_items/widgetable.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Mutations + module WorkItems + module Widgetable + extend ActiveSupport::Concern + + def extract_widget_params(work_item_type, attributes) + # Get the list of widgets for the work item's type to extract only the supported attributes + widget_keys = work_item_type.widgets.map(&:api_symbol) + widget_params = attributes.extract!(*widget_keys) + + # Cannot use prepare to use `.to_h` on each input due to + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87472#note_945199865 + widget_params.transform_values { |values| values.to_h } + end + end + end +end diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb index 2ae26ed0e1a..dcd7d58737a 100644 --- a/app/graphql/mutations/work_items/create.rb +++ b/app/graphql/mutations/work_items/create.rb @@ -7,6 +7,7 @@ module Mutations include Mutations::SpamProtection include FindsProject + include Mutations::WorkItems::Widgetable description "Creates a work item. Available only when feature flag `work_items` is enabled." @@ -15,6 +16,9 @@ module Mutations argument :description, GraphQL::Types::String, required: false, description: copy_field_description(Types::WorkItemType, :description) + argument :hierarchy_widget, ::Types::WorkItems::Widgets::HierarchyCreateInputType, + required: false, + description: 'Input for hierarchy widget.' argument :project_path, GraphQL::Types::ID, required: true, description: 'Full path of the project the work item is associated with.' @@ -36,10 +40,18 @@ module Mutations return { errors: ['`work_items` feature flag disabled for this project'] } end - params = global_id_compatibility_params(attributes).merge(author_id: current_user.id) - spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) - create_result = ::WorkItems::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute + params = global_id_compatibility_params(attributes).merge(author_id: current_user.id) + type = ::WorkItems::Type.find(attributes[:work_item_type_id]) + widget_params = extract_widget_params(type, params) + + create_result = ::WorkItems::CreateService.new( + project: project, + current_user: current_user, + params: params, + spam_params: spam_params, + widget_params: widget_params + ).execute check_spam_action_response!(create_result[:work_item]) if create_result[:work_item] diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb index 8b1968dbad2..c9f733223b5 100644 --- a/app/graphql/mutations/work_items/update.rb +++ b/app/graphql/mutations/work_items/update.rb @@ -9,6 +9,7 @@ module Mutations include Mutations::SpamProtection include Mutations::WorkItems::UpdateArguments + include Mutations::WorkItems::Widgetable authorize :update_work_item @@ -24,7 +25,7 @@ module Mutations end spam_params = ::Spam::SpamParams.new_from_request(request: context[:request]) - widget_params = extract_widget_params(work_item, attributes) + widget_params = extract_widget_params(work_item.work_item_type, attributes) update_result = ::WorkItems::UpdateService.new( project: work_item.project, @@ -47,16 +48,6 @@ module Mutations def find_object(id:) GitlabSchema.find_by_gid(id) end - - def extract_widget_params(work_item, attributes) - # Get the list of widgets for the work item's type to extract only the supported attributes - widget_keys = work_item.work_item_type.widgets.map(&:api_symbol) - widget_params = attributes.extract!(*widget_keys) - - # Cannot use prepare to use `.to_h` on each input due to - # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87472#note_945199865 - widget_params.transform_values { |values| values.to_h } - end end end end diff --git a/app/graphql/resolvers/ci/runners_resolver.rb b/app/graphql/resolvers/ci/runners_resolver.rb index e221dfea4d0..64738608b60 100644 --- a/app/graphql/resolvers/ci/runners_resolver.rb +++ b/app/graphql/resolvers/ci/runners_resolver.rb @@ -36,6 +36,10 @@ module Resolvers required: false, description: 'Sort order of results.' + argument :upgrade_status, ::Types::Ci::RunnerUpgradeStatusTypeEnum, + required: false, + description: 'Filter by upgrade status.' + def resolve_with_lookahead(**args) apply_lookahead( ::Ci::RunnersFinder @@ -54,6 +58,7 @@ module Resolvers status_status: params[:status]&.to_s, type_type: params[:type], tag_name: params[:tag_list], + upgrade_status: params[:upgrade_status], search: params[:search], sort: params[:sort]&.to_s, preload: { diff --git a/app/graphql/types/work_items/widgets/hierarchy_create_input_type.rb b/app/graphql/types/work_items/widgets/hierarchy_create_input_type.rb new file mode 100644 index 00000000000..34cd72bfb3a --- /dev/null +++ b/app/graphql/types/work_items/widgets/hierarchy_create_input_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module Widgets + class HierarchyCreateInputType < BaseInputObject + graphql_name 'WorkItemWidgetHierarchyCreateInput' + + argument :parent_id, ::Types::GlobalIDType[::WorkItem], + required: false, + description: 'Global ID of the parent work item.', + prepare: ->(id, _) { id&.model_id } + end + end + end +end diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb index 469d6c1a7eb..fb8fafe59f3 100644 --- a/app/helpers/nav/new_dropdown_helper.rb +++ b/app/helpers/nav/new_dropdown_helper.rb @@ -16,7 +16,7 @@ module Nav menu_sections.push(general_menu_section) { - title: _("Create new"), + title: _("Create new..."), menu_sections: menu_sections.select { |x| x.fetch(:menu_items).any? } } end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index c3d948ef9fd..f41ad890184 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -78,6 +78,7 @@ module Ci has_many :groups, through: :runner_namespaces, disable_joins: true has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build' + has_one :runner_version, primary_key: :version, foreign_key: :version, class_name: 'Ci::RunnerVersion' before_save :ensure_token @@ -475,6 +476,10 @@ module Ci private + scope :with_upgrade_status, ->(upgrade_status) do + Ci::Runner.joins(:runner_version).where(runner_version: { status: upgrade_status }) + end + EXECUTOR_NAME_TO_TYPES = { 'unknown' => :unknown, 'custom' => :custom, diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 642dd0736f5..aef9526f5be 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class WorkItem < Issue + include Gitlab::Utils::StrongMemoize + self.table_name = 'issues' self.inheritance_column = :_type_disabled @@ -23,8 +25,10 @@ class WorkItem < Issue end def widgets - work_item_type.widgets.map do |widget_class| - widget_class.new(self) + strong_memoize(:widgets) do + work_item_type.widgets.map do |widget_class| + widget_class.new(self) + end end end diff --git a/app/services/concerns/work_items/widgetable_service.rb b/app/services/concerns/work_items/widgetable_service.rb new file mode 100644 index 00000000000..5665b07dce1 --- /dev/null +++ b/app/services/concerns/work_items/widgetable_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module WorkItems + module WidgetableService + def execute_widgets(work_item:, callback:, widget_params: {}) + work_item.widgets.each do |widget| + widget_service(widget).try(callback, params: widget_params[widget.class.api_symbol]) + end + end + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def widget_service(widget) + @widget_services ||= {} + return @widget_services[widget] if @widget_services.has_key?(widget) + + @widget_services[widget] = widget_service_class(widget)&.new(widget: widget, current_user: current_user) + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + def widget_service_class(widget) + "WorkItems::Widgets::#{widget.type.capitalize}Service::#{self.class.name.demodulize}".constantize + rescue NameError + nil + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index a00a9a2021b..544b2170a02 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -231,7 +231,7 @@ class IssuableBaseService < ::BaseProjectService before_create(issuable) issuable_saved = issuable.with_transaction_returning_status do - issuable.save + transaction_create(issuable) end if issuable_saved @@ -339,6 +339,10 @@ class IssuableBaseService < ::BaseProjectService issuable.save(touch: touch) end + def transaction_create(issuable) + issuable.save + end + def update_task(issuable) filter_params(issuable) diff --git a/app/services/work_items/create_service.rb b/app/services/work_items/create_service.rb index 705735fe403..677867eef3f 100644 --- a/app/services/work_items/create_service.rb +++ b/app/services/work_items/create_service.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true module WorkItems - class CreateService + class CreateService < Issues::CreateService include ::Services::ReturnServiceResponses + include WidgetableService - def initialize(project:, current_user: nil, params: {}, spam_params:) - @create_service = ::Issues::CreateService.new( + def initialize(project:, current_user: nil, params: {}, spam_params:, widget_params: {}) + super( project: project, current_user: current_user, params: params, spam_params: spam_params, build_service: ::WorkItems::BuildService.new(project: project, current_user: current_user, params: params) ) - @current_user = current_user - @project = project + @widget_params = widget_params end def execute @@ -21,13 +21,21 @@ module WorkItems return error(_('Operation not allowed'), :forbidden) end - work_item = @create_service.execute + work_item = super if work_item.valid? success(payload(work_item)) else error(work_item.errors.full_messages, :unprocessable_entity, pass_back: payload(work_item)) end + rescue ::WorkItems::Widgets::BaseService::WidgetError => e + error(e.message, :unprocessable_entity) + end + + def transaction_create(work_item) + super + + execute_widgets(work_item: work_item, callback: :after_create_in_transaction, widget_params: @widget_params) end private diff --git a/app/services/work_items/update_service.rb b/app/services/work_items/update_service.rb index 4d34b32096c..98818fda263 100644 --- a/app/services/work_items/update_service.rb +++ b/app/services/work_items/update_service.rb @@ -2,13 +2,14 @@ module WorkItems class UpdateService < ::Issues::UpdateService + include WidgetableService + def initialize(project:, current_user: nil, params: {}, spam_params: nil, widget_params: {}) params[:widget_params] = true if widget_params.present? super(project: project, current_user: current_user, params: params, spam_params: nil) @widget_params = widget_params - @widget_services = {} end def execute(work_item) @@ -26,13 +27,13 @@ module WorkItems private def update(work_item) - execute_widgets(work_item: work_item, callback: :update) + execute_widgets(work_item: work_item, callback: :update, widget_params: @widget_params) super end def transaction_update(work_item, opts = {}) - execute_widgets(work_item: work_item, callback: :before_update_in_transaction) + execute_widgets(work_item: work_item, callback: :before_update_in_transaction, widget_params: @widget_params) super end @@ -43,22 +44,6 @@ module WorkItems GraphqlTriggers.issuable_title_updated(work_item) if work_item.previous_changes.key?(:title) end - def execute_widgets(work_item:, callback:) - work_item.widgets.each do |widget| - widget_service(widget).try(callback, params: @widget_params[widget.class.api_symbol]) - end - end - - def widget_service(widget) - @widget_services[widget] ||= widget_service_class(widget)&.new(widget: widget, current_user: current_user) - end - - def widget_service_class(widget) - "WorkItems::Widgets::#{widget.type.capitalize}Service::UpdateService".constantize - rescue NameError - nil - end - def payload(work_item) { work_item: work_item } end diff --git a/app/services/work_items/widgets/hierarchy_service/base_service.rb b/app/services/work_items/widgets/hierarchy_service/base_service.rb index d385074a9a6..df29beb6bde 100644 --- a/app/services/work_items/widgets/hierarchy_service/base_service.rb +++ b/app/services/work_items/widgets/hierarchy_service/base_service.rb @@ -6,6 +6,38 @@ module WorkItems class BaseService < WorkItems::Widgets::BaseService private + def handle_hierarchy_changes(params) + return feature_flag_error unless feature_flag_enabled? + return incompatible_args_error if incompatible_args?(params) + + update_hierarchy(params) + end + + def update_hierarchy(params) + parent_id = params.delete(:parent_id) + children_ids = params.delete(:children_ids) + + return update_work_item_parent(parent_id) if parent_id + + update_work_item_children(children_ids) if children_ids + end + + def feature_flag_enabled? + Feature.enabled?(:work_items_hierarchy, widget.work_item&.project) + end + + def incompatible_args?(params) + params[:parent_id] && params[:children_ids] + end + + def feature_flag_error + error(_('`work_items_hierarchy` feature flag disabled for this project')) + end + + def incompatible_args_error + error(_('A Work Item can be a parent or a child, but not both.')) + end + def update_work_item_parent(parent_id) begin parent = ::WorkItem.find(parent_id) diff --git a/app/services/work_items/widgets/hierarchy_service/create_service.rb b/app/services/work_items/widgets/hierarchy_service/create_service.rb new file mode 100644 index 00000000000..64e37407b98 --- /dev/null +++ b/app/services/work_items/widgets/hierarchy_service/create_service.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module WorkItems + module Widgets + module HierarchyService + class CreateService < WorkItems::Widgets::HierarchyService::BaseService + def after_create_in_transaction(params:) + return unless params.present? + + result = handle_hierarchy_changes(params) + + raise WidgetError, result[:message] if result[:status] == :error + end + end + end + end +end diff --git a/app/services/work_items/widgets/hierarchy_service/update_service.rb b/app/services/work_items/widgets/hierarchy_service/update_service.rb index 5b6fea6aa1e..a971e38aefe 100644 --- a/app/services/work_items/widgets/hierarchy_service/update_service.rb +++ b/app/services/work_items/widgets/hierarchy_service/update_service.rb @@ -11,40 +11,6 @@ module WorkItems raise WidgetError, result[:message] if result[:status] == :error end - - private - - def handle_hierarchy_changes(params) - return feature_flag_error unless feature_flag_enabled? - return incompatible_args_error if incompatible_args?(params) - - update_hierarchy(params) - end - - def update_hierarchy(params) - parent_id = params.delete(:parent_id) - children_ids = params.delete(:children_ids) - - return update_work_item_parent(parent_id) if parent_id - - update_work_item_children(children_ids) if children_ids - end - - def feature_flag_enabled? - Feature.enabled?(:work_items_hierarchy, widget.work_item&.project) - end - - def incompatible_args?(params) - params[:parent_id] && params[:children_ids] - end - - def feature_flag_error - error(_('`work_items_hierarchy` feature flag disabled for this project')) - end - - def incompatible_args_error - error(_('A Work Item can be a parent or a child, but not both.')) - end end end end diff --git a/app/views/notify/member_access_denied_email.html.haml b/app/views/notify/member_access_denied_email.html.haml index eeef66d353d..98d3daf2107 100644 --- a/app/views/notify/member_access_denied_email.html.haml +++ b/app/views/notify/member_access_denied_email.html.haml @@ -1,12 +1,7 @@ %tr %td.text-content %p - Your request to join the - - - if @source_hidden - #{content_tag :span, 'Hidden', class: :highlight} - - else - #{link_to member_source.human_name, member_source.web_url, class: :highlight} - - #{member_source.model_name.singular} has been #{content_tag :span, 'denied', class: :highlight}. + - target_to_join = @source_hidden ? content_tag(:span, _('Hidden'), class: :highlight) : link_to(member_source.human_name, member_source.web_url, class: :highlight) + - denied_tag = content_tag :span, _('denied'), class: :highlight + = s_('Notify|Your request to join the %{target_to_join} %{target_type} has been %{denied_tag}.').html_safe % { target_to_join: target_to_join, target_type: member_source.model_name.singular, denied_tag: denied_tag } diff --git a/db/docs/broadcast_messages.yml b/db/docs/broadcast_messages.yml index da8693df4fc..1e4c181d48f 100644 --- a/db/docs/broadcast_messages.yml +++ b/db/docs/broadcast_messages.yml @@ -3,7 +3,7 @@ table_name: broadcast_messages classes: - BroadcastMessage feature_categories: -- navigation -description: TODO +- onboarding +description: GitLab can display broadcast messages to users of a GitLab instance introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/f1ecf53c1e55fbbc66cb2d7d12fb411cbfc2ace8 milestone: '6.3' diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 1e63f3fdb04..e93fa43cb03 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -387,6 +387,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. | | `tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). | | `type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. | +| `upgradeStatus` | [`CiRunnerUpgradeStatusType`](#cirunnerupgradestatustype) | Filter by upgrade status. | ### `Query.snippets` @@ -5546,6 +5547,7 @@ Input type: `WorkItemCreateInput` | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `description` | [`String`](#string) | Description of the work item. | +| `hierarchyWidget` | [`WorkItemWidgetHierarchyCreateInput`](#workitemwidgethierarchycreateinput) | Input for hierarchy widget. | | `projectPath` | [`ID!`](#id) | Full path of the project the work item is associated with. | | `title` | [`String!`](#string) | Title of the work item. | | `workItemTypeId` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of a work item type. | @@ -12397,6 +12399,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | `status` | [`CiRunnerStatus`](#cirunnerstatus) | Filter runners by status. | | `tagList` | [`[String!]`](#string) | Filter by tags associated with the runner (comma-separated or array). | | `type` | [`CiRunnerType`](#cirunnertype) | Filter runners by type. | +| `upgradeStatus` | [`CiRunnerUpgradeStatusType`](#cirunnerupgradestatustype) | Filter by upgrade status. | ##### `Group.scanExecutionPolicies` @@ -22110,6 +22113,14 @@ A time-frame defined as a closed inclusive range of two dates. | ---- | ---- | ----------- | | `description` | [`String!`](#string) | Description of the work item. | +### `WorkItemWidgetHierarchyCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `parentId` | [`WorkItemID`](#workitemid) | Global ID of the parent work item. | + ### `WorkItemWidgetHierarchyUpdateInput` #### Arguments diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index b5d68ca5de2..e818cad0d03 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -4,7 +4,7 @@ module API class BroadcastMessages < ::API::Base include PaginationParams - feature_category :navigation + feature_category :onboarding urgency :low resource :broadcast_messages do diff --git a/lib/gitlab/ci/runner_releases.rb b/lib/gitlab/ci/runner_releases.rb index 1930bc4ff75..addaf9a3989 100644 --- a/lib/gitlab/ci/runner_releases.rb +++ b/lib/gitlab/ci/runner_releases.rb @@ -6,42 +6,74 @@ module Gitlab include Singleton RELEASES_VALIDITY_PERIOD = 1.day - RELEASES_VALIDITY_AFTER_ERROR_PERIOD = 5.seconds INITIAL_BACKOFF = 5.seconds MAX_BACKOFF = 1.hour BACKOFF_GROWTH_FACTOR = 2.0 def initialize - reset! + reset_backoff! + end + + def expired? + backoff_active? || !Rails.cache.exist?(cache_key) end # Returns a sorted list of the publicly available GitLab Runner releases # def releases - return @releases unless Time.now.utc >= @expire_time + return if backoff_active? - @releases = fetch_new_releases + Rails.cache.fetch( + cache_key, + skip_nil: true, + expires_in: RELEASES_VALIDITY_PERIOD, + race_condition_ttl: 10.seconds + ) do + response = Gitlab::HTTP.try_get(runner_releases_url) + + unless response.success? + @backoff_expire_time = next_backoff.from_now + break nil + end + + reset_backoff! + extract_releases(response) + end end - def reset! - @expire_time = Time.now.utc - @releases = nil + def reset_backoff! + @backoff_expire_time = nil @backoff_count = 0 end private - def fetch_new_releases - response = Gitlab::HTTP.try_get(::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url) + def runner_releases_url + @runner_releases_url ||= ::Gitlab::CurrentSettings.current_application_settings.public_runner_releases_url + end - releases = response.success? ? extract_releases(response) : nil - ensure - @expire_time = (releases ? RELEASES_VALIDITY_PERIOD : next_backoff).from_now + def cache_key + runner_releases_url + end + + def backoff_active? + return false unless @backoff_expire_time + + Time.now.utc < @backoff_expire_time end def extract_releases(response) - response.parsed_response.map { |release| parse_runner_release(release) }.sort! + return unless response.parsed_response.is_a?(Array) + + releases = response.parsed_response + .map { |release| parse_runner_release(release) } + .select(&:valid?) + .sort! + + return if releases.empty? && response.parsed_response.present? + + releases end def parse_runner_release(release) diff --git a/lib/gitlab/jira_import/issue_serializer.rb b/lib/gitlab/jira_import/issue_serializer.rb index ab748d67fbf..96cae916ed7 100644 --- a/lib/gitlab/jira_import/issue_serializer.rb +++ b/lib/gitlab/jira_import/issue_serializer.rb @@ -5,10 +5,11 @@ module Gitlab class IssueSerializer attr_reader :jira_issue, :project, :import_owner_id, :params, :formatter - def initialize(project, jira_issue, import_owner_id, params = {}) + def initialize(project, jira_issue, import_owner_id, work_item_type_id, params = {}) @jira_issue = jira_issue @project = project @import_owner_id = import_owner_id + @work_item_type_id = work_item_type_id @params = params @formatter = Gitlab::ImportFormatter.new end @@ -24,7 +25,8 @@ module Gitlab created_at: jira_issue.created, author_id: reporter, assignee_ids: assignees, - label_ids: label_ids + label_ids: label_ids, + work_item_type_id: @work_item_type_id } end diff --git a/lib/gitlab/jira_import/issues_importer.rb b/lib/gitlab/jira_import/issues_importer.rb index 8a03162f111..f1ead57c911 100644 --- a/lib/gitlab/jira_import/issues_importer.rb +++ b/lib/gitlab/jira_import/issues_importer.rb @@ -16,6 +16,7 @@ module Gitlab @start_at = Gitlab::JiraImport.get_issues_next_start_at(project.id) @imported_items_cache_key = JiraImport.already_imported_cache_key(:issues, project.id) @job_waiter = JobWaiter.new + @issue_type_id = WorkItems::Type.default_issue_type.id end def execute @@ -58,8 +59,13 @@ module Gitlab next if already_imported?(jira_issue.id) begin - issue_attrs = IssueSerializer.new(project, jira_issue, running_import.user_id, { iid: next_iid }).execute - + issue_attrs = IssueSerializer.new( + project, + jira_issue, + running_import.user_id, + @issue_type_id, + { iid: next_iid } + ).execute Gitlab::JiraImport::ImportIssueWorker.perform_async(project.id, jira_issue.id, issue_attrs, job_waiter.key) job_waiter.jobs_remaining += 1 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bec9843b424..3687df309cc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6261,6 +6261,12 @@ msgstr "" msgid "Billing|You can begin moving members in %{namespaceName} now. A member loses access to the group when you turn off %{strongStart}In a seat%{strongEnd}. If over 5 members have %{strongStart}In a seat%{strongEnd} enabled after June 22, 2022, we'll select the 5 members who maintain access. We'll first count members that have Owner and Maintainer roles, then the most recently active members until we reach 5 members. The remaining members will get a status of Over limit and lose access to the group." msgstr "" +msgid "Billing|Your free group is now limited to %{free_user_limit} members" +msgstr "" + +msgid "Billing|Your group recently changed to use the Free plan. Free groups are limited to %{free_user_limit} members and the remaining members will get a status of over-limit and lose access to the group. You can free up space for new members by removing those who no longer need access or toggling them to over-limit. To get an unlimited number of members, you can %{link_start}upgrade%{link_end} to a paid tier." +msgstr "" + msgid "Bitbucket Server Import" msgstr "" @@ -10812,6 +10818,9 @@ msgstr "" msgid "Create new project" msgstr "" +msgid "Create new..." +msgstr "" + msgid "Create one" msgstr "" @@ -26494,6 +26503,9 @@ msgstr "" msgid "Notify|You don't have access to the project." msgstr "" +msgid "Notify|Your request to join the %{target_to_join} %{target_type} has been %{denied_tag}." +msgstr "" + msgid "Notify|successfully completed %{jobs} in %{stages}." msgstr "" @@ -45554,6 +45566,9 @@ msgstr "" msgid "deleted" msgstr "" +msgid "denied" +msgstr "" + msgid "deploy" msgstr "" diff --git a/spec/features/nav/top_nav_responsive_spec.rb b/spec/features/nav/top_nav_responsive_spec.rb index d571327e4b5..4f8e47b5068 100644 --- a/spec/features/nav/top_nav_responsive_spec.rb +++ b/spec/features/nav/top_nav_responsive_spec.rb @@ -41,7 +41,7 @@ RSpec.describe 'top nav responsive', :js do end it 'has new dropdown', :aggregate_failures do - click_button('Create new') + click_button('Create new...') expect(page).to have_link('New project', href: new_project_path) expect(page).to have_link('New group', href: new_group_path) diff --git a/spec/features/nav/top_nav_tooltip_spec.rb b/spec/features/nav/top_nav_tooltip_spec.rb index 58bfe1caf65..73e4571e7a2 100644 --- a/spec/features/nav/top_nav_tooltip_spec.rb +++ b/spec/features/nav/top_nav_tooltip_spec.rb @@ -15,10 +15,10 @@ RSpec.describe 'top nav tooltips', :js do page.find(btn).hover - expect(page).to have_content('Create new') + expect(page).to have_content('Create new...') page.find(btn).click - expect(page).not_to have_content('Create new') + expect(page).not_to have_content('Create new...') end end diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb index e7ec4f01995..aeab5a51766 100644 --- a/spec/finders/ci/runners_finder_spec.rb +++ b/spec/finders/ci/runners_finder_spec.rb @@ -49,6 +49,67 @@ RSpec.describe Ci::RunnersFinder do end end + context 'by upgrade status' do + let(:upgrade_status) {} + + let_it_be(:runner1) { create(:ci_runner, version: 'a') } + let_it_be(:runner2) { create(:ci_runner, version: 'b') } + let_it_be(:runner3) { create(:ci_runner, version: 'c') } + let_it_be(:runner_version_recommended) do + create(:ci_runner_version, version: 'a', status: :recommended) + end + + let_it_be(:runner_version_not_available) do + create(:ci_runner_version, version: 'b', status: :not_available) + end + + let_it_be(:runner_version_available) do + create(:ci_runner_version, version: 'c', status: :available) + end + + def execute + described_class.new(current_user: admin, params: { upgrade_status: upgrade_status }).execute + end + + Ci::RunnerVersion.statuses.keys.map(&:to_sym).each do |status| + context "set to :#{status}" do + let(:upgrade_status) { status } + + it "calls with_upgrade_status scope with corresponding :#{status} status" do + if [:available, :not_available, :recommended].include?(status) + expected_result = Ci::Runner.with_upgrade_status(status) + end + + expect(Ci::Runner).to receive(:with_upgrade_status).with(status).and_call_original + + result = execute + + expect(result).to match_array(expected_result) if expected_result + end + end + end + + context 'set to an invalid value' do + let(:upgrade_status) { :some_invalid_status } + + it 'does not call with_upgrade_status' do + expect(Ci::Runner).not_to receive(:with_upgrade_status) + + expect(execute).to match_array(Ci::Runner.all) + end + end + + context 'set to nil' do + let(:upgrade_status) { nil } + + it 'does not call with_upgrade_status' do + expect(Ci::Runner).not_to receive(:with_upgrade_status) + + expect(execute).to match_array(Ci::Runner.all) + end + end + end + context 'by status' do Ci::Runner::AVAILABLE_STATUSES.each do |status| it "calls the corresponding :#{status} scope on Ci::Runner" do diff --git a/spec/graphql/resolvers/ci/runners_resolver_spec.rb b/spec/graphql/resolvers/ci/runners_resolver_spec.rb index b1f5f7b3e43..8586d359336 100644 --- a/spec/graphql/resolvers/ci/runners_resolver_spec.rb +++ b/spec/graphql/resolvers/ci/runners_resolver_spec.rb @@ -52,6 +52,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver do { active: true, status: 'active', + upgrade_status: 'recommended', type: :instance_type, tag_list: ['active_runner'], search: 'abc', @@ -63,6 +64,7 @@ RSpec.describe Resolvers::Ci::RunnersResolver do { active: true, status_status: 'active', + upgrade_status: 'recommended', type_type: :instance_type, tag_name: ['active_runner'], preload: { tag_name: nil }, diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb index 4f32ac5b5c6..2fe237fb996 100644 --- a/spec/helpers/nav/new_dropdown_helper_spec.rb +++ b/spec/helpers/nav/new_dropdown_helper_spec.rb @@ -55,7 +55,7 @@ RSpec.describe Nav::NewDropdownHelper do end it 'has title' do - expect(subject[:title]).to eq('Create new') + expect(subject[:title]).to eq('Create new...') end context 'when current_user is nil (anonymous)' do diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb index eb29629730c..95396a883fa 100644 --- a/spec/lib/gitlab/ci/runner_releases_spec.rb +++ b/spec/lib/gitlab/ci/runner_releases_spec.rb @@ -5,12 +5,14 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::RunnerReleases do subject { described_class.instance } - describe '#releases' do - before do - subject.reset! + let(:runner_releases_url) { 'the release API URL' } - stub_application_setting(public_runner_releases_url: 'the release API URL') - allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response(response) } + describe '#releases', :use_clean_rails_memory_store_caching do + before do + subject.reset_backoff! + + stub_application_setting(public_runner_releases_url: runner_releases_url) + allow(Gitlab::HTTP).to receive(:try_get).with(runner_releases_url).once { mock_http_response(response) } end def releases @@ -40,7 +42,9 @@ RSpec.describe Gitlab::Ci::RunnerReleases do releases travel followup_request_interval do - expect(Gitlab::HTTP).to receive(:try_get).with('the release API URL').once { mock_http_response(followup_response) } + expect(Gitlab::HTTP).to receive(:try_get) + .with(runner_releases_url) + .once { mock_http_response(followup_response) } expect(releases).to eq((expected_result || []) + [Gitlab::VersionInfo.new(14, 9, 2)]) end @@ -62,14 +66,14 @@ RSpec.describe Gitlab::Ci::RunnerReleases do start_time = Time.now.utc.change(usec: 0) http_call_timestamp_offsets = [] - allow(Gitlab::HTTP).to receive(:try_get).with('the release API URL') do + allow(Gitlab::HTTP).to receive(:try_get).with(runner_releases_url) do http_call_timestamp_offsets << Time.now.utc - start_time mock_http_response(response) end # An initial HTTP request fails travel_to(start_time) - subject.reset! + subject.reset_backoff! expect(releases).to be_nil # Successive failed requests result in HTTP requests only after specific backoff periods @@ -86,7 +90,7 @@ RSpec.describe Gitlab::Ci::RunnerReleases do # Finally a successful HTTP request results in releases being returned allow(Gitlab::HTTP).to receive(:try_get) - .with('the release API URL') + .with(runner_releases_url) .once { mock_http_response([{ 'name' => 'v14.9.1-beta1-ee' }]) } travel 1.hour expect(releases).not_to be_nil @@ -109,13 +113,58 @@ RSpec.describe Gitlab::Ci::RunnerReleases do it_behaves_like 'requests that follow cache status', 1.day end - def mock_http_response(response) - http_response = instance_double(HTTParty::Response) + context 'when response contains unexpected input type' do + let(:response) { 'error' } - allow(http_response).to receive(:success?).and_return(response.present?) - allow(http_response).to receive(:parsed_response).and_return(response) + it { expect(releases).to be_nil } + end - http_response + context 'when response contains unexpected input array' do + let(:response) { ['error'] } + + it { expect(releases).to be_nil } end end + + describe '#expired?', :use_clean_rails_memory_store_caching do + def expired? + described_class.instance.expired? + end + + before do + stub_application_setting(public_runner_releases_url: runner_releases_url) + + subject.send(:reset_backoff!) + end + + it { expect(expired?).to be_truthy } + + it 'behaves appropriately in refetch' do + allow(Gitlab::HTTP).to receive(:try_get).with(runner_releases_url).once { mock_http_response([]) } + + subject.releases + expect(expired?).to be_falsey + + travel Gitlab::Ci::RunnerReleases::RELEASES_VALIDITY_PERIOD + 1.second do + expect(expired?).to be_truthy + + allow(Gitlab::HTTP).to receive(:try_get).with(runner_releases_url).once { mock_http_response(nil) } + subject.releases + expect(expired?).to be_truthy + + allow(Gitlab::HTTP).to receive(:try_get).with(runner_releases_url).once { mock_http_response([]) } + subject.releases + expect(expired?).to be_truthy + end + end + end + + def mock_http_response(response) + http_response = instance_double(HTTParty::Response) + + allow(http_response).to receive(:success?).and_return(!response.nil?) + allow(http_response).to receive(:parsed_response).and_return(response) + + http_response + end end diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb index 198d2db234c..f93835f4429 100644 --- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb +++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb @@ -11,6 +11,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') } let_it_be(:current_user) { create(:user) } let_it_be(:user) { create(:user) } + let_it_be(:issue_type_id) { WorkItems::Type.default_issue_type.id } let(:iid) { 5 } let(:key) { 'PROJECT-5' } @@ -54,7 +55,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do let(:params) { { iid: iid } } - subject { described_class.new(project, jira_issue, current_user.id, params).execute } + subject { described_class.new(project, jira_issue, current_user.id, issue_type_id, params).execute } let(:expected_description) do <<~MD @@ -81,7 +82,8 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do created_at: created_at, author_id: current_user.id, assignee_ids: nil, - label_ids: [project_label.id, group_label.id] + Label.reorder(id: :asc).last(2).pluck(:id) + label_ids: [project_label.id, group_label.id] + Label.reorder(id: :asc).last(2).pluck(:id), + work_item_type_id: issue_type_id ) end diff --git a/spec/lib/gitlab/jira_import/issues_importer_spec.rb b/spec/lib/gitlab/jira_import/issues_importer_spec.rb index 565a9ad17e1..1bc052ee0b6 100644 --- a/spec/lib/gitlab/jira_import/issues_importer_spec.rb +++ b/spec/lib/gitlab/jira_import/issues_importer_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Gitlab::JiraImport::IssuesImporter do let_it_be(:project) { create(:project) } let_it_be(:jira_import) { create(:jira_import_state, project: project, user: current_user) } let_it_be(:jira_integration) { create(:jira_integration, project: project) } + let_it_be(:default_issue_type_id) { WorkItems::Type.default_issue_type.id } subject { described_class.new(project) } @@ -47,12 +48,22 @@ RSpec.describe Gitlab::JiraImport::IssuesImporter do count.times do |i| if raise_exception_on_even_mocks && i.even? - expect(Gitlab::JiraImport::IssueSerializer).to receive(:new) - .with(project, jira_issues[i], current_user.id, { iid: next_iid + 1 }).and_raise('Some error') + expect(Gitlab::JiraImport::IssueSerializer).to receive(:new).with( + project, + jira_issues[i], + current_user.id, + default_issue_type_id, + { iid: next_iid + 1 } + ).and_raise('Some error') else next_iid += 1 - expect(Gitlab::JiraImport::IssueSerializer).to receive(:new) - .with(project, jira_issues[i], current_user.id, { iid: next_iid }).and_return(serializer) + expect(Gitlab::JiraImport::IssueSerializer).to receive(:new).with( + project, + jira_issues[i], + current_user.id, + default_issue_type_id, + { iid: next_iid } + ).and_return(serializer) end end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 63f2731e4d1..2fbfbbaf830 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -1767,4 +1767,39 @@ RSpec.describe Ci::Runner do end end end + + describe '#with_upgrade_status' do + subject { described_class.with_upgrade_status(upgrade_status) } + + let_it_be(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') } + let_it_be(:runner_14_1_0) { create(:ci_runner, version: '14.1.0') } + let_it_be(:runner_14_1_1) { create(:ci_runner, version: '14.1.1') } + let_it_be(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :available) } + let_it_be(:runner_version_14_1_0) { create(:ci_runner_version, version: '14.1.0', status: :recommended) } + let_it_be(:runner_version_14_1_1) { create(:ci_runner_version, version: '14.1.1', status: :not_available) } + + context ':not_available' do + let(:upgrade_status) { :not_available } + + it 'returns runners whose version is assigned :not_available' do + is_expected.to contain_exactly(runner_14_1_1) + end + end + + context ':available' do + let(:upgrade_status) { :available } + + it 'returns runners whose version is assigned :available' do + is_expected.to contain_exactly(runner_14_0_0) + end + end + + context ':recommended' do + let(:upgrade_status) { :recommended} + + it 'returns runners whose version is assigned :recommended' do + is_expected.to contain_exactly(runner_14_1_0) + end + end + end end diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index df9be2616c5..b6cb790bb71 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -133,7 +133,7 @@ RSpec.describe API::API do 'meta.caller_id' => 'GET /api/:version/broadcast_messages', 'meta.remote_ip' => an_instance_of(String), 'meta.client_id' => a_string_matching(%r{\Aip/.+}), - 'meta.feature_category' => 'navigation', + 'meta.feature_category' => 'onboarding', 'route' => '/api/:version/broadcast_messages') expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user') @@ -209,7 +209,7 @@ RSpec.describe API::API do 'meta.caller_id' => 'GET /api/:version/broadcast_messages', 'meta.remote_ip' => an_instance_of(String), 'meta.client_id' => a_string_matching(%r{\Aip/.+}), - 'meta.feature_category' => 'navigation', + 'meta.feature_category' => 'onboarding', 'route' => '/api/:version/broadcast_messages') expect(data.stringify_keys).not_to include('meta.project', 'meta.root_namespace', 'meta.user') diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index 6abdaa2c850..ee818d9e37c 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -63,6 +63,70 @@ RSpec.describe 'Create a work item' do let(:mutation_class) { ::Mutations::WorkItems::Create } end + context 'with hierarchy widget input' do + let(:widgets_response) { mutation_response['workItem']['widgets'] } + let(:fields) do + <<~FIELDS + workItem { + widgets { + type + ... on WorkItemWidgetHierarchy { + parent { + id + } + children { + edges { + node { + id + } + } + } + } + } + } + errors + FIELDS + end + + let(:mutation) { graphql_mutation(:workItemCreate, input.merge('projectPath' => project.full_path), fields) } + + context 'when setting parent' do + let_it_be(:parent) { create(:work_item, project: project) } + + let(:input) do + { + title: 'item1', + workItemTypeId: WorkItems::Type.default_by_type(:task).to_global_id.to_s, + hierarchyWidget: { 'parentId' => parent.to_global_id.to_s } + } + end + + it 'updates the work item parent' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(widgets_response).to include( + { + 'children' => { 'edges' => [] }, + 'parent' => { 'id' => parent.to_global_id.to_s }, + 'type' => 'HIERARCHY' + } + ) + end + + context 'when parent work item type is invalid' do + let_it_be(:parent) { create(:work_item, :task, project: project) } + + it 'returns error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['errors']).to contain_exactly(/cannot be added: Only Issue can be parent of Task./) + expect(mutation_response['workItem']).to be_nil + end + end + end + end + context 'when the work_items feature flag is disabled' do before do stub_feature_flags(work_items: false) diff --git a/spec/services/work_items/create_service_spec.rb b/spec/services/work_items/create_service_spec.rb index f495e967b26..2cf5c60abd9 100644 --- a/spec/services/work_items/create_service_spec.rb +++ b/spec/services/work_items/create_service_spec.rb @@ -9,6 +9,7 @@ RSpec.describe WorkItems::CreateService do let_it_be(:guest) { create(:user) } let_it_be(:user_with_no_access) { create(:user) } + let(:widget_params) { {} } let(:spam_params) { double } let(:current_user) { guest } let(:opts) do @@ -23,7 +24,15 @@ RSpec.describe WorkItems::CreateService do end describe '#execute' do - subject(:service_result) { described_class.new(project: project, current_user: current_user, params: opts, spam_params: spam_params).execute } + subject(:service_result) do + described_class.new( + project: project, + current_user: current_user, + params: opts, + spam_params: spam_params, + widget_params: widget_params + ).execute + end before do stub_spam_services @@ -80,5 +89,79 @@ RSpec.describe WorkItems::CreateService do service_result end end + + it_behaves_like 'work item widgetable service' do + let(:widget_params) do + { + hierarchy_widget: { parent_id: 1 } + } + end + + let(:service) do + described_class.new( + project: project, + current_user: current_user, + params: opts, + spam_params: spam_params, + widget_params: widget_params + ) + end + + let(:service_execute) { service.execute } + + let(:supported_widgets) do + [ + { klass: WorkItems::Widgets::HierarchyService::CreateService, callback: :after_create_in_transaction, params: { parent_id: 1 } } + ] + end + end + + describe 'hierarchy widget' do + context 'when parent is valid work item' do + let_it_be(:parent) { create(:work_item, project: project) } + + let(:widget_params) { { hierarchy_widget: { parent_id: parent.id } } } + + let(:opts) do + { + title: 'Awesome work_item', + description: 'please fix', + work_item_type: create(:work_item_type, :task) + } + end + + it 'creates new work item and sets parent reference' do + expect { service_result }.to change( + WorkItem, :count).by(1).and(change( + WorkItems::ParentLink, :count).by(1)) + + expect(service_result[:status]).to be(:success) + end + + context 'when parent type is invalid' do + let_it_be(:parent) { create(:work_item, :task, project: project) } + + it 'does not create new work item if parent can not be set' do + expect { service_result }.not_to change(WorkItem, :count) + + expect(service_result[:status]).to be(:error) + expect(service_result[:message]).to match(/Only Issue can be parent of Task./) + end + end + + context 'when hiearchy feature flag is disabled' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + it 'does not create new work item if parent can not be set' do + expect { service_result }.not_to change(WorkItem, :count) + + expect(service_result[:status]).to be(:error) + expect(service_result[:message]).to eq('`work_items_hierarchy` feature flag disabled for this project') + end + end + end + end end end diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb index d7c5dd71503..247d10fe245 100644 --- a/spec/services/work_items/update_service_spec.rb +++ b/spec/services/work_items/update_service_spec.rb @@ -77,6 +77,36 @@ RSpec.describe WorkItems::UpdateService do end end + it_behaves_like 'work item widgetable service' do + let(:widget_params) do + { + hierarchy_widget: { parent_id: 1 }, + description_widget: { description: 'foo' }, + weight_widget: { weight: 1 } + } + end + + let(:service) do + described_class.new( + project: project, + current_user: current_user, + params: opts, + spam_params: spam_params, + widget_params: widget_params + ) + end + + let(:service_execute) { service.execute(work_item) } + + let(:supported_widgets) do + [ + { klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :update, params: { description: 'foo' } }, + { klass: WorkItems::Widgets::WeightService::UpdateService, callback: :update, params: { weight: 1 } }, + { klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent_id: 1 } } + ] + end + end + context 'when updating widgets' do let(:widget_service_class) { WorkItems::Widgets::DescriptionService::UpdateService } let(:widget_params) { { description_widget: { description: 'changed' } } } diff --git a/spec/support/shared_examples/work_items/widgetable_service_shared_examples.rb b/spec/support/shared_examples/work_items/widgetable_service_shared_examples.rb new file mode 100644 index 00000000000..491662d17d3 --- /dev/null +++ b/spec/support/shared_examples/work_items/widgetable_service_shared_examples.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'work item widgetable service' do + it 'executes callbacks for expected widgets' do + supported_widgets.each do |widget| + expect_next_instance_of(widget[:klass]) do |widget_instance| + expect(widget_instance).to receive(widget[:callback]).with(params: widget[:params]) + end + end + + service_execute + end +end