Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
157c4d9279
commit
d67ccb290a
32 changed files with 348 additions and 93 deletions
|
@ -148,5 +148,5 @@ codebase.
|
|||
/chatops run feature set <feature-flag-name> false
|
||||
```
|
||||
|
||||
/label ~"feature flag"
|
||||
/label ~"feature flag" ~"type::feature" ~"feature::addition"
|
||||
/assign DRI
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import { GlButton, GlModalDirective } from '@gitlab/ui';
|
||||
import { formType } from '~/boards/constants';
|
||||
import eventHub from '~/boards/eventhub';
|
||||
import { s__ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GitlabExperiment,
|
||||
},
|
||||
directives: {
|
||||
GlModalDirective,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
inject: ['multipleIssueBoardsAvailable', 'canAdminBoard'],
|
||||
computed: {
|
||||
canShowCreateButton() {
|
||||
return this.canAdminBoard && this.multipleIssueBoardsAvailable;
|
||||
},
|
||||
createButtonText() {
|
||||
return s__('Boards|New board');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showDialog() {
|
||||
this.track('click_button', { label: 'create_board' });
|
||||
eventHub.$emit('showBoardModal', formType.new);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gitlab-experiment name="prominent_create_board_btn">
|
||||
<template #control> </template>
|
||||
<template #candidate>
|
||||
<div v-if="canShowCreateButton" class="gl-ml-1 gl-mr-3 gl-display-flex gl-align-items-center">
|
||||
<gl-button data-qa-selector="new_board_button" @click.prevent="showDialog">
|
||||
{{ createButtonText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
</gitlab-experiment>
|
||||
</template>
|
|
@ -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();
|
||||
|
|
29
app/assets/javascripts/boards/new_board.js
Normal file
29
app/assets/javascripts/boards/new_board.js
Normal file
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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' } }
|
||||
|
|
|
@ -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
|
|
@ -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")
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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`)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
71
spec/frontend/boards/components/new_board_button_spec.js
Normal file
71
spec/frontend/boards/components/new_board_button_spec.js
Normal file
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
|
||||
.to match(%r{<li><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue