From d67ccb290ab5e54ff42f9cd464a132fc1d3067e1 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 2 Nov 2021 12:12:25 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../issue_templates/Feature Flag Roll Out.md | 2 +- .rubocop_manual_todo.yml | 1 - .../boards/components/new_board_button.vue | 47 ++++++++++++ app/assets/javascripts/boards/index.js | 2 + app/assets/javascripts/boards/new_board.js | 29 ++++++++ app/assets/javascripts/breadcrumb.js | 18 +++-- .../javascripts/vue_shared/constants.js | 3 + app/controllers/groups/boards_controller.rb | 4 ++ app/controllers/projects/boards_controller.rb | 4 ++ app/controllers/projects/tags_controller.rb | 3 +- app/helpers/breadcrumbs_helper.rb | 8 +-- app/helpers/groups_helper.rb | 4 +- app/helpers/user_callouts_helper.rb | 21 ------ app/helpers/wiki_helper.rb | 6 +- app/models/namespace.rb | 6 +- app/views/layouts/nav/_breadcrumbs.html.haml | 2 +- .../breadcrumbs/_collapsed_dropdown.html.haml | 11 --- .../_collapsed_inline_list.html.haml | 11 +++ .../shared/issuable/_search_bar.html.haml | 1 + .../experiment/prominent_create_board_btn.yml | 8 +++ .../additional_snowplow_tracking.yml | 4 +- doc/development/pipelines.md | 26 ++++++- .../groups/loaders/group_loader.rb | 29 +++++++- .../testing/request_inspector_middleware.rb | 4 +- lib/gitlab/tracking.rb | 2 +- locale/gitlab.pot | 15 ++-- .../components/new_board_button_spec.js | 71 +++++++++++++++++++ spec/helpers/groups_helper_spec.rb | 2 +- spec/helpers/user_callouts_helper_spec.rb | 14 ---- spec/helpers/wiki_helper_spec.rb | 6 +- .../groups/loaders/group_loader_spec.rb | 58 +++++++++++++-- spec/models/namespace_spec.rb | 19 +++++ 32 files changed, 348 insertions(+), 93 deletions(-) create mode 100644 app/assets/javascripts/boards/components/new_board_button.vue create mode 100644 app/assets/javascripts/boards/new_board.js delete mode 100644 app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml create mode 100644 app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml create mode 100644 config/feature_flags/experiment/prominent_create_board_btn.yml rename config/feature_flags/{development => ops}/additional_snowplow_tracking.yml (83%) create mode 100644 spec/frontend/boards/components/new_board_button_spec.js diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md index 00b396bac4e..76dd8077889 100644 --- a/.gitlab/issue_templates/Feature Flag Roll Out.md +++ b/.gitlab/issue_templates/Feature Flag Roll Out.md @@ -148,5 +148,5 @@ codebase. /chatops run feature set false ``` -/label ~"feature flag" +/label ~"feature flag" ~"type::feature" ~"feature::addition" /assign DRI diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml index c460dbe7fe1..d1c2fff0528 100644 --- a/.rubocop_manual_todo.yml +++ b/.rubocop_manual_todo.yml @@ -2211,7 +2211,6 @@ Performance/OpenStruct: - 'lib/gitlab/ci/ansi2html.rb' - 'lib/gitlab/git/diff_collection.rb' - 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb' - - 'lib/gitlab/testing/request_inspector_middleware.rb' - 'lib/mattermost/session.rb' # WIP: https://gitlab.com/gitlab-org/gitlab/-/issues/324629 diff --git a/app/assets/javascripts/boards/components/new_board_button.vue b/app/assets/javascripts/boards/components/new_board_button.vue new file mode 100644 index 00000000000..f7914c636cc --- /dev/null +++ b/app/assets/javascripts/boards/components/new_board_button.vue @@ -0,0 +1,47 @@ + + + diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index be363e8f05f..66b0562d02d 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -16,6 +16,7 @@ import toggleFocusMode from '~/boards/toggle_focus'; import { NavigationType, parseBoolean } from '~/lib/utils/common_utils'; import { fullBoardId } from './boards_util'; import boardConfigToggle from './config_toggle'; +import initNewBoard from './new_board'; import { gqlClient } from './graphql'; import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher'; @@ -130,6 +131,7 @@ export default () => { } boardConfigToggle(); + initNewBoard(); toggleFocusMode(); toggleLabels(); diff --git a/app/assets/javascripts/boards/new_board.js b/app/assets/javascripts/boards/new_board.js new file mode 100644 index 00000000000..34f2fea79a9 --- /dev/null +++ b/app/assets/javascripts/boards/new_board.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import { getExperimentVariant } from '~/experimentation/utils'; +import { CANDIDATE_VARIANT } from '~/experimentation/constants'; +import NewBoardButton from './components/new_board_button.vue'; + +export default () => { + if (getExperimentVariant('prominent_create_board_btn') !== CANDIDATE_VARIANT) { + return; + } + + const el = document.querySelector('.js-new-board'); + + if (!el) { + return; + } + + // eslint-disable-next-line no-new + new Vue({ + el, + provide: { + multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleIssueBoardsAvailable), + canAdminBoard: parseBoolean(el.dataset.canAdminBoard), + }, + render(h) { + return h(NewBoardButton); + }, + }); +}; diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js index b8b8a0b2867..b9d3742974c 100644 --- a/app/assets/javascripts/breadcrumb.js +++ b/app/assets/javascripts/breadcrumb.js @@ -1,5 +1,4 @@ import $ from 'jquery'; -import { hide } from '~/tooltips'; export const addTooltipToEl = (el) => { const textEl = el.querySelector('.js-breadcrumb-item-text'); @@ -19,16 +18,23 @@ export default () => { .filter((el) => !el.classList.contains('dropdown')) .map((el) => el.querySelector('a')) .filter((el) => el); - const $expander = $('.js-breadcrumbs-collapsed-expander'); + const $expanderBtn = $('.js-breadcrumbs-collapsed-expander'); topLevelLinks.forEach((el) => addTooltipToEl(el)); - $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', (e) => { - const $el = $('.js-breadcrumbs-collapsed-expander', e.currentTarget); + $expanderBtn.on('click', () => { + const detailItems = $('.breadcrumbs-detail-item'); + const hiddenClass = 'gl-display-none!'; - $el.toggleClass('open'); + $.each(detailItems, (_key, item) => { + $(item).toggleClass(hiddenClass); + }); - hide($el); + // remove the ellipsis + $('li.expander').remove(); + + // set focus on first breadcrumb item + $('.breadcrumb-item-text').first().focus(); }); } }; diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js index 33fac5ebdbb..9cb66f6e65f 100644 --- a/app/assets/javascripts/vue_shared/constants.js +++ b/app/assets/javascripts/vue_shared/constants.js @@ -63,3 +63,6 @@ export const timeRanges = [ export const defaultTimeRange = timeRanges.find((tr) => tr.default); export const getTimeWindow = (timeWindowName) => timeRanges.find((tr) => tr.name === timeWindowName); + +export const AVATAR_SHAPE_OPTION_CIRCLE = 'circle'; +export const AVATAR_SHAPE_OPTION_RECT = 'rect'; diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index 14dfd3f8178..3152c4d733f 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -12,6 +12,10 @@ class Groups::BoardsController < Groups::ApplicationController push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml) push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml) + experiment(:prominent_create_board_btn, subject: current_user) do |e| + e.use { } + e.try { } + end.run end feature_category :team_planning diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index bffaa12302a..7354c2c71ac 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -12,6 +12,10 @@ class Projects::BoardsController < Projects::ApplicationController push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml) push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml) push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml) + experiment(:prominent_create_board_btn, subject: current_user) do |e| + e.use { } + e.try { } + end.run end feature_category :team_planning diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 02d36c3353d..581680f1b13 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -10,8 +10,7 @@ class Projects::TagsController < Projects::ApplicationController before_action :authorize_download_code! before_action :authorize_admin_tag!, only: [:new, :create, :destroy] - feature_category :source_code_management, [:index, :show, :new, :destroy] - feature_category :release_evidence, [:create] + feature_category :source_code_management # rubocop: disable CodeReuse/ActiveRecord def index diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb index ade7c48b03f..3a622a65685 100644 --- a/app/helpers/breadcrumbs_helper.rb +++ b/app/helpers/breadcrumbs_helper.rb @@ -27,10 +27,10 @@ module BreadcrumbsHelper end end - def add_to_breadcrumb_dropdown(link, location: :before) - @breadcrumb_dropdown_links ||= {} - @breadcrumb_dropdown_links[location] ||= [] - @breadcrumb_dropdown_links[location] << link + def add_to_breadcrumb_collapsed_links(link, location: :before) + @breadcrumb_collapsed_links ||= {} + @breadcrumb_collapsed_links[location] ||= [] + @breadcrumb_collapsed_links[location] << link end def push_to_schema_breadcrumb(text, link) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 30aaa0a5acc..9ba7d004d6c 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -39,7 +39,7 @@ module GroupsHelper sorted_ancestors(group).with_route.reverse_each.with_index do |parent, index| if index > 0 - add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before) + add_to_breadcrumb_collapsed_links(group_title_link(parent), location: :before) else full_title << breadcrumb_list_item(group_title_link(parent, hidable: false)) end @@ -47,7 +47,7 @@ module GroupsHelper push_to_schema_breadcrumb(simple_sanitize(parent.name), group_path(parent)) end - full_title << render("layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups")) + full_title << render("layouts/nav/breadcrumbs/collapsed_inline_list", location: :before, title: _("Show all breadcrumbs")) full_title << breadcrumb_list_item(group_title_link(group)) push_to_schema_breadcrumb(simple_sanitize(group.name), group_path(group)) diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index 1c67ca983fa..d8e69145c40 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -81,32 +81,11 @@ module UserCalloutsHelper def user_dismissed_for_group(feature_name, group, ignore_dismissal_earlier_than = nil) return false unless current_user - set_dismissed_from_cookie(group) - current_user.dismissed_callout_for_group?(feature_name: feature_name, group: group, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than) end - def set_dismissed_from_cookie(group) - # bridge function for one milestone to try and not annoy users who might have already dismissed this alert - # remove in 14.4 or 14.5? https://gitlab.com/gitlab-org/gitlab/-/issues/340322 - dismissed_key = "invite_#{group.id}_#{current_user.id}" - - if cookies[dismissed_key].present? - params = { - feature_name: INVITE_MEMBERS_BANNER, - group_id: group.id - } - - Users::DismissGroupCalloutService.new( - container: nil, current_user: current_user, params: params - ).execute - - cookies.delete dismissed_key - end - end - def just_created? flash[:notice]&.include?('successfully created') end diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 1b0d1254dc8..ba876f6cb65 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -9,7 +9,7 @@ module WikiHelper if page.persisted? titles << page.human_title breadcrumb_title(page.human_title) - wiki_breadcrumb_dropdown_links(page.slug) + wiki_breadcrumb_collapsed_links(page.slug) end titles << action if action @@ -39,14 +39,14 @@ module WikiHelper .join(' / ') end - def wiki_breadcrumb_dropdown_links(page_slug) + def wiki_breadcrumb_collapsed_links(page_slug) page_slug_split = page_slug.split('/') page_slug_split.pop(1) current_slug = "" page_slug_split .map do |dir_or_page| current_slug = "#{current_slug}#{dir_or_page}/" - add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, wiki_page_path(@wiki, current_slug)), location: :after + add_to_breadcrumb_collapsed_links link_to(WikiPage.unhyphenize(dir_or_page).capitalize, wiki_page_path(@wiki, current_slug)), location: :after end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 4b4e7f5cc1a..80abb733418 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -125,7 +125,7 @@ class Namespace < ApplicationRecord scope :user_namespaces, -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) } # TODO: this can be simplified with `type != 'Project'` when working on issue # https://gitlab.com/gitlab-org/gitlab/-/issues/341070 - scope :without_project_namespaces, -> { where("type IS DISTINCT FROM ?", Namespaces::ProjectNamespace.sti_name) } + scope :without_project_namespaces, -> { where(Namespace.arel_table[:type].is_distinct_from(Namespaces::ProjectNamespace.sti_name)) } scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) } scope :include_route, -> { includes(:route) } scope :by_parent, -> (parent) { where(parent_id: parent) } @@ -192,9 +192,9 @@ class Namespace < ApplicationRecord # Returns an ActiveRecord::Relation. def search(query, include_parents: false) if include_parents - where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id)) + without_project_namespaces.where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id)) else - fuzzy_search(query, [:path, :name]) + without_project_namespaces.fuzzy_search(query, [:path, :name]) end end diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index ab4c0989eab..3c52c430868 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -16,7 +16,7 @@ - if @breadcrumbs_extra_links - @breadcrumbs_extra_links.each do |extra| = breadcrumb_list_item link_to(extra[:text], extra[:link]) - = render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after + = render "layouts/nav/breadcrumbs/collapsed_inline_list", location: :after - unless @skip_current_level_breadcrumb %li %h2.breadcrumbs-sub-title{ data: { qa_selector: 'breadcrumb_sub_title_content' } } diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml deleted file mode 100644 index 8ea75087fed..00000000000 --- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -- dropdown_location = local_assigns.fetch(:location, nil) -- button_tooltip = local_assigns.fetch(:title, _("Show parent pages")) -- if defined?(@breadcrumb_dropdown_links) && @breadcrumb_dropdown_links.key?(dropdown_location) - %li.dropdown - %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip } - = sprite_icon("ellipsis_h", size: 12) - = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle") - .dropdown-menu - %ul - - @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index| - %li{ style: "text-indent: #{[index * 16, 60].min}px;" }= link diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml new file mode 100644 index 00000000000..ccb660c050e --- /dev/null +++ b/app/views/layouts/nav/breadcrumbs/_collapsed_inline_list.html.haml @@ -0,0 +1,11 @@ +- dropdown_location = local_assigns.fetch(:location, nil) +- button_tooltip = local_assigns.fetch(:title, _("Show all breadcrumbs")) +- if defined?(@breadcrumb_collapsed_links) && @breadcrumb_collapsed_links.key?(dropdown_location) + %li.expander + %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { container: "body" }, "aria-label": button_tooltip, title: button_tooltip } + = sprite_icon("ellipsis_h", size: 12) + = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle") + - @breadcrumb_collapsed_links[dropdown_location].each_with_index do |link, index| + %li{ :class => "gl-display-none! breadcrumbs-detail-item" } + = link + = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle") diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 81a7581d392..e097852216c 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -15,6 +15,7 @@ .d-flex.flex-column.flex-md-row.flex-grow-1.mb-lg-0.mb-md-2.mb-sm-0.w-100 - if type == :boards = render "shared/boards/switcher", board: board + .js-new-board{ data: { multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s, can_admin_board: can?(current_user, :admin_issue_board, parent).to_s, } } = form_tag page_filter_path, method: :get, class: 'filter-form js-filter-form w-100' do - if params[:search].present? = hidden_field_tag :search, params[:search] diff --git a/config/feature_flags/experiment/prominent_create_board_btn.yml b/config/feature_flags/experiment/prominent_create_board_btn.yml new file mode 100644 index 00000000000..b54dd25be15 --- /dev/null +++ b/config/feature_flags/experiment/prominent_create_board_btn.yml @@ -0,0 +1,8 @@ +--- +name: prominent_create_board_btn +introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72683" +rollout_issue_url: "https://gitlab.com/gitlab-org/gitlab/-/issues/343415" +milestone: "14.5" +type: experiment +group: group::product planning +default_enabled: false diff --git a/config/feature_flags/development/additional_snowplow_tracking.yml b/config/feature_flags/ops/additional_snowplow_tracking.yml similarity index 83% rename from config/feature_flags/development/additional_snowplow_tracking.yml rename to config/feature_flags/ops/additional_snowplow_tracking.yml index 0d021a2f8b0..4dbf91bbff5 100644 --- a/config/feature_flags/development/additional_snowplow_tracking.yml +++ b/config/feature_flags/ops/additional_snowplow_tracking.yml @@ -1,8 +1,8 @@ --- name: additional_snowplow_tracking introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/12088 -rollout_issue_url: +rollout_issue_url: milestone: '11.11' -type: development group: group::product intelligence default_enabled: false +type: ops diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index aef29bd4f21..e1e13340812 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -23,6 +23,30 @@ as much as possible. After a merge request has been approved, the pipeline would contain the full RSpec & Jest tests. This will ensure that all tests have been run before a merge request is merged. +### Overview of the GitLab project test dependency + +To understand how the minimal test jobs are executed, we need to understand the dependency between +GitLab code (frontend and backend) and the respective tests (Jest and RSpec). +This dependency can be visualized in the following diagram: + +```mermaid +flowchart LR + subgraph frontend + fe["Frontend code"]--tested with-->jest + end + subgraph backend + be["Backend code"]--tested with-->rspec + end + + be--generates-->fixtures["frontend fixtures"] + fixtures--used in-->jest +``` + +In summary: + +- RSpec tests are dependent on the backend code. +- Jest tests are dependent on both frontend and backend code, the latter through the frontend fixtures. + ### RSpec minimal jobs #### Determining related RSpec test files in a merge request @@ -57,7 +81,7 @@ In this mode, `jest` would resolve all the dependencies of related to the change In addition, there are a few circumstances where we would always run the full Jest tests: -- when the `pipeline:run-all-rspec` label is set on the merge request +- when the `pipeline:run-all-jest` label is set on the merge request - when the merge request is created by an automation (e.g. Gitaly update or MR targeting a stable branch) - when any CI config file is changed (i.e. `.gitlab-ci.yml` or `.gitlab/ci/**/*`) - when any frontend "core" file is changed (i.e. `package.json`, `yarn.lock`, `babel.config.js`, `jest.config.*.js`, `config/helpers/**/*.js`) diff --git a/lib/bulk_imports/groups/loaders/group_loader.rb b/lib/bulk_imports/groups/loaders/group_loader.rb index a631685c2ad..5f5307123a5 100644 --- a/lib/bulk_imports/groups/loaders/group_loader.rb +++ b/lib/bulk_imports/groups/loaders/group_loader.rb @@ -4,10 +4,21 @@ module BulkImports module Groups module Loaders class GroupLoader - def load(context, data) - return unless user_can_create_group?(context.current_user, data) + GroupCreationError = Class.new(StandardError) - group = ::Groups::CreateService.new(context.current_user, data).execute + def load(context, data) + path = data['path'] + current_user = context.current_user + destination_namespace = context.entity.destination_namespace + + raise(GroupCreationError, 'Path is missing') unless path.present? + raise(GroupCreationError, 'Destination is not a group') if user_namespace_destination?(destination_namespace) + raise(GroupCreationError, 'User not allowed to create group') unless user_can_create_group?(current_user, data) + raise(GroupCreationError, 'Group exists') if group_exists?(destination_namespace, path) + + group = ::Groups::CreateService.new(current_user, data).execute + + raise(GroupCreationError, group.errors.full_messages.to_sentence) if group.errors.any? context.entity.update!(group: group) @@ -25,6 +36,18 @@ module BulkImports Ability.allowed?(current_user, :create_group) end end + + def group_exists?(destination_namespace, path) + full_path = destination_namespace.present? ? File.join(destination_namespace, path) : path + + Group.find_by_full_path(full_path).present? + end + + def user_namespace_destination?(destination_namespace) + return false unless destination_namespace.present? + + Namespace.find_by_full_path(destination_namespace)&.user_namespace? + end end end end diff --git a/lib/gitlab/testing/request_inspector_middleware.rb b/lib/gitlab/testing/request_inspector_middleware.rb index 36cdfebcc28..3cbe97cd84c 100644 --- a/lib/gitlab/testing/request_inspector_middleware.rb +++ b/lib/gitlab/testing/request_inspector_middleware.rb @@ -9,6 +9,8 @@ module Gitlab @@logged_requests = Concurrent::Array.new @@inject_headers = Concurrent::Hash.new + Request = Struct.new(:url, :status_code, :request_headers, :response_headers, :body, keyword_init: true) + # Resets the current request log and starts logging requests def self.log_requests!(headers = {}) @@inject_headers.replace(headers) @@ -40,7 +42,7 @@ module Gitlab full_body = +'' body.each { |b| full_body << b } - request = OpenStruct.new( + request = Request.new( url: url, status_code: status, request_headers: request_headers, diff --git a/lib/gitlab/tracking.rb b/lib/gitlab/tracking.rb index c9478e13d25..1f280765bf1 100644 --- a/lib/gitlab/tracking.rb +++ b/lib/gitlab/tracking.rb @@ -18,7 +18,7 @@ module Gitlab end def options(group) - additional_features = Feature.enabled?(:additional_snowplow_tracking, group) + additional_features = Feature.enabled?(:additional_snowplow_tracking, group, type: :ops) { namespace: SNOWPLOW_NAMESPACE, hostname: Gitlab::CurrentSettings.snowplow_collector_hostname, diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a30639a84af..81a293ac667 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5624,6 +5624,9 @@ msgstr "" msgid "Boards|Failed to fetch blocking %{issuableType}s" msgstr "" +msgid "Boards|New board" +msgstr "" + msgid "Boards|New epic" msgstr "" @@ -23910,6 +23913,9 @@ msgstr "" msgid "OnCallSchedules|Your schedule has been successfully created. To add individual users to this schedule, use the Add a rotation button. To enable notifications for this schedule, you must also create an %{linkStart}escalation policy%{linkEnd}." msgstr "" +msgid "OnDemandScans|Could not fetch on-demand scans. Please refresh the page, or try again later." +msgstr "" + msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later." msgstr "" @@ -31611,6 +31617,9 @@ msgstr "" msgid "Show all activity" msgstr "" +msgid "Show all breadcrumbs" +msgstr "" + msgid "Show all issues." msgstr "" @@ -31665,12 +31674,6 @@ msgstr "" msgid "Show one file at a time" msgstr "" -msgid "Show parent pages" -msgstr "" - -msgid "Show parent subgroups" -msgstr "" - msgid "Show the Closed list" msgstr "" diff --git a/spec/frontend/boards/components/new_board_button_spec.js b/spec/frontend/boards/components/new_board_button_spec.js new file mode 100644 index 00000000000..e66c31953f6 --- /dev/null +++ b/spec/frontend/boards/components/new_board_button_spec.js @@ -0,0 +1,71 @@ +import { mount } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import NewBoardButton from '~/boards/components/new_board_button.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { assignGitlabExperiment } from 'helpers/experimentation_helper'; +import eventHub from '~/boards/eventhub'; + +const FEATURE = 'prominent_create_board_btn'; + +describe('NewBoardButton', () => { + let wrapper; + + const createComponent = (args = {}) => + extendedWrapper( + mount(NewBoardButton, { + provide: { + canAdminBoard: true, + multipleIssueBoardsAvailable: true, + ...args, + }, + }), + ); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('control variant', () => { + assignGitlabExperiment(FEATURE, 'control'); + + it('renders nothing', () => { + wrapper = createComponent(); + + expect(wrapper.text()).toBe(''); + }); + }); + + describe('candidate variant', () => { + assignGitlabExperiment(FEATURE, 'candidate'); + + it('renders New board button when `candidate` variant', () => { + wrapper = createComponent(); + + expect(wrapper.text()).toBe('New board'); + }); + + it('renders nothing when `canAdminBoard` is `false`', () => { + wrapper = createComponent({ canAdminBoard: false }); + + expect(wrapper.find(GlButton).exists()).toBe(false); + }); + + it('renders nothing when `multipleIssueBoardsAvailable` is `false`', () => { + wrapper = createComponent({ multipleIssueBoardsAvailable: false }); + + expect(wrapper.find(GlButton).exists()).toBe(false); + }); + + it('emits `showBoardModal` when button is clicked', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(); + + wrapper = createComponent(); + + wrapper.find(GlButton).vm.$emit('click', { preventDefault: () => {} }); + + expect(eventHub.$emit).toHaveBeenCalledWith('showBoardModal', 'new'); + }); + }); +}); diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 4d647696130..8859ed27022 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -92,7 +92,7 @@ RSpec.describe GroupsHelper do shared_examples 'correct ancestor order' do it 'outputs the groups in the correct order' do expect(subject) - .to match(%r{
  • #{deep_nested_group.name}.*
  • .*#{very_deep_nested_group.name}}m) + .to match(%r{
  • #{deep_nested_group.name}.*
  • .*#{very_deep_nested_group.name}}m) end end diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb index f738ba855b8..7abc67e29a4 100644 --- a/spec/helpers/user_callouts_helper_spec.rb +++ b/spec/helpers/user_callouts_helper_spec.rb @@ -216,20 +216,6 @@ RSpec.describe UserCalloutsHelper do context 'when the invite_members_banner has not been dismissed' do it { is_expected.to eq(true) } - context 'when a user has dismissed this banner via cookies already' do - before do - helper.request.cookies["invite_#{group.id}_#{user.id}"] = 'true' - end - - it { is_expected.to eq(false) } - - it 'creates the callout from cookie', :aggregate_failures do - expect { subject }.to change { Users::GroupCallout.count }.by(1) - expect(Users::GroupCallout.last).to have_attributes(group_id: group.id, - feature_name: described_class::INVITE_MEMBERS_BANNER) - end - end - context 'when the group was just created' do before do flash[:notice] = "Group #{group.name} was successfully created" diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb index dc76f92db1b..0d04ca2b876 100644 --- a/spec/helpers/wiki_helper_spec.rb +++ b/spec/helpers/wiki_helper_spec.rb @@ -8,7 +8,7 @@ RSpec.describe WikiHelper do it 'sets the title for the show action' do expect(helper).to receive(:breadcrumb_title).with(page.human_title) - expect(helper).to receive(:wiki_breadcrumb_dropdown_links).with(page.slug) + expect(helper).to receive(:wiki_breadcrumb_collapsed_links).with(page.slug) expect(helper).to receive(:page_title).with(page.human_title, 'Wiki') expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki)) @@ -17,7 +17,7 @@ RSpec.describe WikiHelper do it 'sets the title for a custom action' do expect(helper).to receive(:breadcrumb_title).with(page.human_title) - expect(helper).to receive(:wiki_breadcrumb_dropdown_links).with(page.slug) + expect(helper).to receive(:wiki_breadcrumb_collapsed_links).with(page.slug) expect(helper).to receive(:page_title).with('Edit', page.human_title, 'Wiki') expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki)) @@ -27,7 +27,7 @@ RSpec.describe WikiHelper do it 'sets the title for an unsaved page' do expect(page).to receive(:persisted?).and_return(false) expect(helper).not_to receive(:breadcrumb_title) - expect(helper).not_to receive(:wiki_breadcrumb_dropdown_links) + expect(helper).not_to receive(:wiki_breadcrumb_collapsed_links) expect(helper).to receive(:page_title).with('Wiki') expect(helper).to receive(:add_to_breadcrumbs).with('Wiki', helper.wiki_path(page.wiki)) diff --git a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb index de0b56045b3..69363bf0866 100644 --- a/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb +++ b/spec/lib/bulk_imports/groups/loaders/group_loader_spec.rb @@ -11,20 +11,66 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } let(:service_double) { instance_double(::Groups::CreateService) } - let(:data) { { foo: :bar } } + let(:data) { { 'path' => 'test' } } subject { described_class.new } + context 'when path is missing' do + it 'raises an error' do + expect { subject.load(context, {}) }.to raise_error(described_class::GroupCreationError, 'Path is missing') + end + end + + context 'when destination namespace is not a group' do + it 'raises an error' do + entity.update!(destination_namespace: user.namespace.path) + + expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'Destination is not a group') + end + end + + context 'when group exists' do + it 'raises an error' do + group1 = create(:group) + group2 = create(:group, parent: group1) + entity.update!(destination_namespace: group1.full_path) + data = { 'path' => group2.path } + + expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'Group exists') + end + end + + context 'when there are other group errors' do + it 'raises an error with those errors' do + group = ::Group.new + group.validate + expected_errors = group.errors.full_messages.to_sentence + + expect(::Groups::CreateService) + .to receive(:new) + .with(context.current_user, data) + .and_return(service_double) + + expect(service_double).to receive(:execute).and_return(group) + expect(entity).not_to receive(:update!) + + expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, expected_errors) + end + end + context 'when user can create group' do shared_examples 'calls Group Create Service to create a new group' do it 'calls Group Create Service to create a new group' do + group_double = instance_double(::Group) + expect(::Groups::CreateService) .to receive(:new) .with(context.current_user, data) .and_return(service_double) - expect(service_double).to receive(:execute) - expect(entity).to receive(:update!) + expect(service_double).to receive(:execute).and_return(group_double) + expect(group_double).to receive(:errors).and_return([]) + expect(entity).to receive(:update!).with(group: group_double) subject.load(context, data) end @@ -40,7 +86,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do context 'when there is parent group' do let(:parent) { create(:group) } - let(:data) { { 'parent_id' => parent.id } } + let(:data) { { 'parent_id' => parent.id, 'path' => 'test' } } before do allow(Ability).to receive(:allowed?).with(user, :create_subgroup, parent).and_return(true) @@ -55,7 +101,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do it 'does not create new group' do expect(::Groups::CreateService).not_to receive(:new) - subject.load(context, data) + expect { subject.load(context, data) }.to raise_error(described_class::GroupCreationError, 'User not allowed to create group') end end @@ -69,7 +115,7 @@ RSpec.describe BulkImports::Groups::Loaders::GroupLoader do context 'when there is parent group' do let(:parent) { create(:group) } - let(:data) { { 'parent_id' => parent.id } } + let(:data) { { 'parent_id' => parent.id, 'path' => 'test' } } before do allow(Ability).to receive(:allowed?).with(user, :create_subgroup, parent).and_return(false) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 808343ffae9..4dae5b5201e 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -559,6 +559,25 @@ RSpec.describe Namespace do it 'returns namespaces with a matching route path regardless of the casing' do expect(described_class.search('PARENT-PATH/NEW-PATH', include_parents: true)).to eq([second_group]) end + + context 'with project namespaces' do + let_it_be(:project) { create(:project, namespace: parent_group, path: 'some-new-path') } + let_it_be(:project_namespace) { create(:project_namespace, project: project) } + + it 'does not return project namespace' do + search_result = described_class.search('path') + + expect(search_result).not_to include(project_namespace) + expect(search_result).to match_array([first_group, parent_group, second_group]) + end + + it 'does not return project namespace when including parents' do + search_result = described_class.search('path', include_parents: true) + + expect(search_result).not_to include(project_namespace) + expect(search_result).to match_array([first_group, parent_group, second_group]) + end + end end describe '.with_statistics' do