Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-05-29 15:07:27 +00:00
parent b2d5b8e4a6
commit 815a08defc
41 changed files with 371 additions and 329 deletions

View File

@ -282,9 +282,6 @@ export default {
'ee/app/assets/javascripts/hand_raise_leads/hand_raise_lead/components/hand_raise_lead_button.vue',
'ee/app/assets/javascripts/hand_raise_leads/hand_raise_lead/components/hand_raise_lead_modal.vue',
'ee/app/assets/javascripts/insights/components/insights_chart.vue',
'ee/app/assets/javascripts/integrations/edit/components/google_artifact_management/configuration_instructions.vue',
'ee/app/assets/javascripts/integrations/edit/components/jira_issue_creation_vulnerabilities.vue',
'ee/app/assets/javascripts/integrations/zentao/issues_show/components/sidebar/zentao_issues_sidebar_root.vue',
'ee/app/assets/javascripts/invite_members/components/invite_modal_base.vue',
'ee/app/assets/javascripts/iterations/components/iteration_cadence_list_item.vue',
'ee/app/assets/javascripts/iterations/components/iteration_form.vue',

View File

@ -5,7 +5,6 @@ import {
initSuperSidebarToggle,
initPageBreadcrumbs,
getSuperSidebarData,
initAdvancedSearchModal,
} from '~/super_sidebar/super_sidebar_bundle';
const superSidebarData = getSuperSidebarData();
@ -13,4 +12,3 @@ const superSidebarData = getSuperSidebarData();
initSuperSidebar(superSidebarData);
initSuperSidebarToggle();
initPageBreadcrumbs();
initAdvancedSearchModal(superSidebarData);

View File

@ -1,84 +0,0 @@
<script>
import { GlModalDirective, GlIcon, GlButton, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { InternalEvents } from '~/tracking';
import {
isNarrowScreen,
isNarrowScreenAddListener,
isNarrowScreenRemoveListener,
} from '~/lib/utils/css_utils';
import { SEARCH_MODAL_ID } from '../constants';
import SearchModal from './global_search.vue';
const trackingMixin = InternalEvents.mixin();
export default {
SEARCH_MODAL_ID,
components: {
GlIcon,
SearchModal,
GlButton,
GlSprintf,
},
i18n: {
searchKbdHelp: s__('GlobalSearch|Search or go to… (or use the / keyboard shortcut)'),
searchBtnText: s__('GlobalSearch|Search or go to… %{kbdStart}/%{kbdEnd}'),
},
directives: {
GlModal: GlModalDirective,
},
mixins: [glFeatureFlagsMixin(), trackingMixin],
data() {
return {
isNarrowScreen: false,
};
},
mounted() {
this.isNarrowScreen = isNarrowScreen();
isNarrowScreenAddListener(this.handleNarrowScreenChange);
},
beforeDestroy() {
isNarrowScreenRemoveListener(this.handleNarrowScreenChange);
},
methods: {
handleNarrowScreenChange({ matches }) {
this.isNarrowScreen = matches;
},
},
};
</script>
<template>
<div
v-if="glFeatures.searchButtonTopRight"
:class="{ 'border-0 gl-rounded-base': !isNarrowScreen }"
>
<gl-button
id="super-sidebar-search"
v-gl-modal="$options.SEARCH_MODAL_ID"
class="gl-relative focus:!gl-focus"
:title="$options.i18n.searchKbdHelp"
:aria-label="$options.i18n.searchKbdHelp"
:class="
isNarrowScreen
? 'shadow-none bg-transparent gl-border gl-w-6 !gl-p-0'
: 'user-bar-button gl-w-full !gl-justify-start !gl-pr-15'
"
data-testid="super-sidebar-search-button"
@click="trackEvent('click_search_button_to_activate_command_palette', { label: 'top_right' })"
>
<gl-icon name="search" />
<span v-if="!isNarrowScreen">
<gl-sprintf :message="$options.i18n.searchBtnText">
<template #kbd="{ content }">
<span class="gl-absolute gl-right-4"
><kbd>{{ content }}</kbd></span
>
</template>
</gl-sprintf>
</span>
</gl-button>
<search-modal />
</div>
</template>

View File

@ -206,7 +206,7 @@ export default {
/>
</div>
<div v-if="!glFeatures.searchButtonTopRight" class="gl-grow">
<div class="gl-grow">
<gl-button
id="super-sidebar-search"
v-gl-tooltip.bottom.html="searchTooltip"

View File

@ -11,7 +11,6 @@ import {
} from './super_sidebar_collapsed_state_manager';
import SuperSidebar from './components/super_sidebar.vue';
import SuperSidebarToggle from './components/super_sidebar_toggle.vue';
import AdvancedSearchModal from './components/global_search/components/global_search_header_app.vue';
export { initPageBreadcrumbs } from './super_sidebar_breadcrumbs';
@ -197,58 +196,3 @@ export const initSuperSidebarToggle = () => {
},
});
};
export function initAdvancedSearchModal({
rootPath,
isSaas,
sidebarData,
searchPath,
issuesPath,
mrPath,
autocompletePath,
searchContext,
projectsPath,
groupsPath,
projectFilesPath,
projectBlobPath,
commandPaletteCommands,
commandPaletteLinks,
contextSwitcherLinks,
isGroup,
}) {
const el = document.querySelector('#js-advanced-search-modal');
if (!el) return false;
return new Vue({
el,
name: 'SuperSidebarRoot',
apolloProvider,
provide: {
rootPath,
commandPaletteCommands,
commandPaletteLinks,
contextSwitcherLinks,
autocompletePath,
searchContext,
projectFilesPath,
projectBlobPath,
projectsPath,
groupsPath,
fullPath: sidebarData.work_items?.full_path,
isGroup,
isSaas: parseBoolean(isSaas),
},
store: createStore({
searchPath,
issuesPath,
mrPath,
autocompletePath,
searchContext,
search: '',
}),
render(h) {
return h(AdvancedSearchModal);
},
});
}

View File

@ -227,11 +227,6 @@ export default {
required: false,
default: '',
},
addPadding: {
type: Boolean,
required: false,
default: false,
},
detailLoading: {
type: Boolean,
required: false,
@ -329,21 +324,19 @@ export default {
<template>
<div class="issuable-list-container">
<issuable-tabs
:add-padding="addPadding"
:tabs="tabs"
:tab-counts="tabCounts"
:current-tab="currentTab"
:truncate-counts="truncateCounts"
@click="$emit('click-tab', $event)"
>
<template #nav-actions>
<slot name="nav-actions"></slot>
</template>
<template #title>
<slot name="title"></slot>
</template>
</issuable-tabs>
<slot name="list-header">
<issuable-tabs
:tabs="tabs"
:tab-counts="tabCounts"
:current-tab="currentTab"
:truncate-counts="truncateCounts"
@click="$emit('click-tab', $event)"
>
<template #nav-actions>
<slot name="nav-actions"></slot>
</template>
</issuable-tabs>
</slot>
<filtered-search-bar
:namespace="namespace"
:recent-searches-storage-key="recentSearchesStorageKey"

View File

@ -28,11 +28,6 @@ export default {
required: false,
default: false,
},
addPadding: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
isTabActive(tabName) {
@ -50,8 +45,8 @@ export default {
<template>
<div class="top-area">
<slot name="title"></slot>
<gl-tabs
v-if="tabs.length > 0"
class="mobile-separator issuable-state-filters gl-m-0 gl-flex gl-grow gl-p-0"
nav-class="gl-border-b-0"
>
@ -75,7 +70,7 @@ export default {
</template>
</gl-tab>
</gl-tabs>
<div :class="['nav-controls', { 'gl-py-3': addPadding }]">
<div class="nav-controls">
<slot name="nav-actions"></slot>
</div>
</div>

View File

@ -0,0 +1,8 @@
<template>
<div class="gl-border-b gl-flex gl-flex-col gl-py-5 sm:gl-flex-row">
<h1 class="gl-heading-2 gl-flex-1 sm:gl-mb-0" data-testid="work-item-list-title">
{{ s__('WorkItem|Work items') }}
</h1>
<slot></slot>
</div>
</template>

View File

@ -84,6 +84,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CreateWorkItemModal from '../components/create_work_item_modal.vue';
import WorkItemHealthStatus from '../components/work_item_health_status.vue';
import WorkItemDrawer from '../components/work_item_drawer.vue';
import WorkItemListHeading from '../components/work_item_list_heading.vue';
import {
BASE_ALLOWED_CREATE_TYPES,
DETAIL_VIEW_QUERY_PARAM_NAME,
@ -131,6 +132,7 @@ export default {
EmptyStateWithoutAnyIssues,
CreateWorkItemModal,
LocalBoard,
WorkItemListHeading,
},
mixins: [glFeatureFlagMixin()],
inject: [
@ -628,6 +630,9 @@ export default {
enableClientSideBoardsExperiment() {
return this.glFeatures.workItemsClientSideBoards;
},
isPlanningViewsEnabled() {
return this.glFeatures.workItemPlanningView;
},
preselectedWorkItemType() {
return this.isEpicsList ? WORK_ITEM_TYPE_NAME_EPIC : WORK_ITEM_TYPE_NAME_ISSUE;
},
@ -638,6 +643,7 @@ export default {
if (!this.hasAnyIssues) {
this.isInitialLoadComplete = false;
}
this.$apollo.queries.workItemStateCounts.refetch();
this.$apollo.queries.workItemsFull.refetch();
this.$apollo.queries.workItemsSlim.refetch();
},
@ -708,7 +714,7 @@ export default {
},
calculateDocumentTitle(data) {
const middleCrumb = this.isGroup ? data.group.name : data.project.name;
if (this.glFeatures.workItemPlanningView) {
if (this.isPlanningViewsEnabled) {
return `${s__('WorkItem|Work items')} · ${middleCrumb} · GitLab`;
}
if (this.isGroup && this.isEpicsList) {
@ -953,7 +959,6 @@ export default {
/>
<issuable-list
:active-issuable="activeItem"
:add-padding="!withTabs"
:current-tab="state"
:default-page-size="pageSize"
:error="error"
@ -988,7 +993,7 @@ export default {
@sort="handleSort"
@select-issuable="handleToggle"
>
<template #nav-actions>
<template v-if="!isPlanningViewsEnabled" #nav-actions>
<div class="gl-flex gl-gap-3">
<gl-button
v-if="enableClientSideBoardsExperiment"
@ -1016,6 +1021,36 @@ export default {
</div>
</template>
<template v-if="isPlanningViewsEnabled" #list-header>
<work-item-list-heading>
<div class="gl-flex gl-gap-3">
<gl-button
v-if="enableClientSideBoardsExperiment"
data-testid="show-local-board-button"
@click="showLocalBoard = true"
>
{{ __('Launch board') }}
</gl-button>
<gl-button
v-if="allowBulkEditing"
:disabled="showBulkEditSidebar"
data-testid="bulk-edit-start-button"
@click="showBulkEditSidebar = true"
>
{{ __('Bulk edit') }}
</gl-button>
<create-work-item-modal
v-if="showNewWorkItem"
:allowed-work-item-types="allowedWorkItemTypes"
:always-show-work-item-type-select="!isEpicsList"
:is-group="isGroup"
:preselected-work-item-type="preselectedWorkItemType"
@workItemCreated="refetchItems"
/>
</div>
</work-item-list-heading>
</template>
<template #timeframe="{ issuable = {} }">
<issue-card-time-info
:issue="issuable"

View File

@ -530,7 +530,7 @@ $command-palette-spacing: px-to-rem(14px);
}
@include gl-media-breakpoint-up(sm) {
padding: 0.2rem 1rem 0;
padding: 5rem 1rem 0;
}
.vertical-align-normalization {

View File

@ -17,6 +17,7 @@ module Groups
push_force_frontend_feature_flag(:continue_indented_text, !!group&.continue_indented_text_feature_flag_enabled?)
push_frontend_feature_flag(:issues_list_drawer, group)
push_frontend_feature_flag(:work_item_status_feature_flag, group&.root_ancestor)
push_frontend_feature_flag(:work_item_planning_view, group)
end
before_action :handle_new_work_item_path, only: [:show]

View File

@ -121,5 +121,22 @@ module Emails
subject: subject(title)
)
end
def import_source_user_complete(source_user_id)
@source_user = Import::SourceUser.find(source_user_id)
@reassign_to_user = @source_user.reassign_to_user
# We don't need to check if admin bypass is fully enabled, only if this was sent due to admin or group bypass
@admin_bypass_enabled = Gitlab::CurrentSettings.allow_bypass_placeholder_confirmation
title = safe_format(
s_('UserMapping|Reassignments in %{group} completed'),
group: @source_user.namespace.full_path
)
email_with_layout(
to: @reassign_to_user.notification_email_or_default,
subject: subject(title)
)
end
end
end

View File

@ -452,6 +452,12 @@ class NotifyPreview < ActionMailer::Preview
Notify.import_source_user_rejected(source_user.id)
end
def import_source_user_complete
source_user = Import::SourceUser.last
Notify.import_source_user_complete(source_user.id)
end
def repository_rewrite_history_success_email
Notify.repository_rewrite_history_success_email(project, user)
end

View File

@ -18,14 +18,23 @@ module Import
return ServiceResponse.success if Gitlab::GitalyClient::RemoteService.exists?(uri.to_s) # rubocop: disable CodeReuse/ActiveRecord -- false positive
end
failure_response
rescue GRPC::BadStatus
# There are a several subclasses of GRPC::BadStatus, but in our case the
# scenario we're interested in the presence of a valid, accessible
# repository, so this treats them all as equivalent.
failure_response
end
private
def failure_response
ServiceResponse.error(
message: 'Unable to access repository with the URL and credentials provided',
reason: 400
)
end
private
def ensure_auth_credentials!
return unless user && password

View File

@ -1 +0,0 @@
%div{ id: 'js-advanced-search-modal' }

View File

@ -4,5 +4,4 @@
= render Pajamas::ButtonComponent.new(icon: 'sidebar', category: :tertiary, button_options: { class: 'js-super-sidebar-toggle-expand super-sidebar-toggle -gl-ml-3', aria: { controls: 'super-sidebar', expanded: 'false', label: _('Primary navigation sidebar') } })
= render "layouts/nav/breadcrumbs/breadcrumbs"
.gl-flex-none.gl-flex.gl-items-center.gl-justify-center.gl-gap-3
= render "layouts/nav/search_button"
= render_if_exists "layouts/nav/ask_duo_button"

View File

@ -0,0 +1,42 @@
- source_hostname = @source_user.source_hostname
- source_name = @source_user.source_name
- source_username = "@#{@source_user.source_username}"
- reassign_to_name = @reassign_to_user.name
- reassign_to_username = link_to @reassign_to_user.to_reference, user_url(@reassign_to_user)
- reassigned_by = @source_user.reassigned_by_user
- reassigned_by_name = reassigned_by.name
- reassigned_by_username = link_to reassigned_by.to_reference, user_url(reassigned_by)
- destination_group = "#{@source_user.namespace.name} (/#{@source_user.namespace.full_path})"
- text_style = 'font-size:16px; text-align:center; line-height:24px; margin-top: 24px;'
- heading_style = 'font-size:14px; line-height:20px; margin-top: 16px; margin-bottom: 8px;'
- details_text_style = 'font-size:14px; line-height:20px; margin-top: 8px; margin-bottom: 8px;'
%p{ style: text_style }
%span= safe_format(s_('UserMapping|You\'ve been reassigned contributions and memberships in %{destination_group} on %{source_hostname}.'),
source_hostname: source_hostname,
destination_group: destination_group)
- if @admin_bypass_enabled
%span= safe_format(s_('UserMapping|For more information, contact %{reassigned_by_name} or another administrator.'),
reassigned_by_name: reassigned_by_name)
- else
%span= safe_format(s_('UserMapping|For more information, contact %{reassigned_by_name} or another group owner.'),
reassigned_by_name: reassigned_by_name)
%hr
%h5{ style: heading_style }
= s_('UserMapping|Import details:')
%p{ style: details_text_style }
= safe_format(s_('UserMapping|Imported from: %{source_hostname}'), source_hostname: source_hostname)
%p{ style: details_text_style }
= safe_format(s_('UserMapping|Imported to: %{destination_group}'), destination_group: destination_group)
%h5{ style: heading_style }
= s_('UserMapping|Reassignment details:')
%p{ style: details_text_style }
= safe_format(s_('UserMapping|Reassigned from: %{source_name} (%{source_username})'), source_name: source_name, source_username: source_username)
%p{ style: details_text_style }
= safe_format(s_('UserMapping|Reassigned to: %{reassign_to_name} (%{reassign_to_username})'), reassign_to_name: reassign_to_name, reassign_to_username: reassign_to_username)
%p{ style: details_text_style }
= safe_format(s_('UserMapping|Reassigned by: %{reassigned_by_name} (%{reassigned_by_username})'), reassigned_by_name: reassigned_by_name, reassigned_by_username: reassigned_by_username)

View File

@ -0,0 +1,31 @@
<% source_hostname = @source_user.source_hostname %>
<% source_name = @source_user.source_name %>
<% source_username = "@#{@source_user.source_username}" %>
<% reassign_to_name = @reassign_to_user.name %>
<% reassign_to_username = "#{@reassign_to_user.to_reference} - #{user_url(@reassign_to_user)}" %>
<% reassigned_by = @source_user.reassigned_by_user %>
<% reassigned_by_name = reassigned_by.name %>
<% reassigned_by_username = "#{reassigned_by.to_reference} - #{user_url(reassigned_by)}" %>
<% destination_group = "#{@source_user.namespace.name} (/#{@source_user.namespace.full_path})" %>
<%= s_('UserMapping|You\'ve been reassigned contributions and memberships in %{destination_group} on %{source_hostname}.') % { reassigned_by_name: reassigned_by_name,
reassigned_by_username: reassigned_by_username,
source_name: source_name,
source_username: source_username,
source_hostname: source_hostname,
destination_group: destination_group } %>
<% if @admin_bypass_enabled %>
<%= s_('UserMapping|For more information, contact %{reassigned_by_name} or another administrator.') %
{ reassigned_by_name: reassigned_by_name } %>
<% else %>
<%= s_('UserMapping|For more information, contact %{reassigned_by_name} or another group owner.') %
{ reassigned_by_name: reassigned_by_name } %>
<% end %>
<%= s_('UserMapping|Import details:') %>
<%= safe_format(s_('UserMapping|Imported from: %{source_hostname}'), source_hostname: source_hostname) %>
<%= safe_format(s_('UserMapping|Imported to: %{destination_group}'), destination_group: destination_group) %>
<%= s_('UserMapping|Reassignment details:') %>
<%= safe_format(s_('UserMapping|Reassigned from: %{source_name} (%{source_username})'), source_name: source_name, source_username: source_username) %>
<%= safe_format(s_('UserMapping|Reassigned to: %{reassign_to_name} (%{reassign_to_username})'), reassign_to_name: reassign_to_name, reassign_to_username: reassign_to_username) %>
<%= safe_format(s_('UserMapping|Reassigned by: %{reassigned_by_name} (%{reassigned_by_username})'), reassigned_by_name: reassigned_by_name, reassigned_by_username: reassigned_by_username) %>

View File

@ -29,8 +29,12 @@ module Import
return self.class.perform_in(BACKOFF_PERIOD, import_source_user.id, params)
end
Import::DeletePlaceholderUserWorker.perform_async(import_source_user.placeholder_user_id,
type: 'placeholder_user')
Import::DeletePlaceholderUserWorker.perform_async(
import_source_user.placeholder_user_id,
{ 'type' => 'placeholder_user' }
)
send_reassign_complete_email if params['confirmation_skipped'] && import_source_user.reassign_to_user.active?
end
def perform_failure(exception, import_source_user_id)
@ -54,6 +58,10 @@ module Import
false
end
def send_reassign_complete_email
Notify.import_source_user_complete(import_source_user.id).deliver_later
end
def log_and_fail_reassignment(exception)
::Import::Framework::Logger.error(
message: 'Failed to reassign placeholder user',

View File

@ -1,9 +0,0 @@
---
name: search_button_top_right
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/480341
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141513
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/18537
milestone: '17.5'
group: group::global search
type: beta
default_enabled: false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -88,7 +88,6 @@ module Gitlab
push_frontend_feature_flag(:remove_monitor_metrics)
push_frontend_feature_flag(:work_items_view_preference, current_user)
push_frontend_feature_flag(:work_item_view_for_issues)
push_frontend_feature_flag(:search_button_top_right, current_user)
push_frontend_feature_flag(:merge_request_dashboard, current_user, type: :wip)
push_frontend_feature_flag(:new_project_creation_form, current_user, type: :wip)
push_frontend_feature_flag(:work_items_client_side_boards, current_user)

View File

@ -29039,12 +29039,6 @@ msgstr ""
msgid "GlobalSearch|Search labels"
msgstr ""
msgid "GlobalSearch|Search or go to… %{kbdStart}/%{kbdEnd}"
msgstr ""
msgid "GlobalSearch|Search or go to… (or use the / keyboard shortcut)"
msgstr ""
msgid "GlobalSearch|Search results are loading"
msgstr ""
@ -66389,6 +66383,12 @@ msgstr ""
msgid "UserMapping|Drop your file here or %{linkStart}click to upload%{linkEnd}."
msgstr ""
msgid "UserMapping|For more information, contact %{reassigned_by_name} or another administrator."
msgstr ""
msgid "UserMapping|For more information, contact %{reassigned_by_name} or another group owner."
msgstr ""
msgid "UserMapping|For more information, see %{link_start}accept contribution reassignment%{link_end}. %{strong_open}If you do not recognize this request, %{report_link_start}report abuse%{report_link_end}.%{strong_close}"
msgstr ""
@ -66503,6 +66503,9 @@ msgstr ""
msgid "UserMapping|Reassigned by: %{reassigned_by_name} (%{reassigned_by_username})"
msgstr ""
msgid "UserMapping|Reassigned from: %{source_name} (%{source_username})"
msgstr ""
msgid "UserMapping|Reassigned to"
msgstr ""
@ -66551,6 +66554,9 @@ msgstr ""
msgid "UserMapping|Reassignments cannot be undone, so check all data carefully before you continue."
msgstr ""
msgid "UserMapping|Reassignments in %{group} completed"
msgstr ""
msgid "UserMapping|Reassignments in %{group} rejected"
msgstr ""
@ -66647,6 +66653,9 @@ msgstr ""
msgid "UserMapping|You must upload a CSV file with a .csv file extension."
msgstr ""
msgid "UserMapping|You've been reassigned contributions and memberships in %{destination_group} on %{source_hostname}."
msgstr ""
msgid "UserProfile|%{count} %{file}"
msgstr ""

View File

@ -165,15 +165,7 @@ RSpec.describe "Admin::Projects", feature_category: :groups_and_projects do
end
describe 'project edit', :js do
before do
resize_window(1920, 1080)
end
after do
restore_window_size
end
it 'shows all breadcrumbs' do
it 'shows breadcrumbs' do
project_params = { id: project.to_param, namespace_id: project.namespace.to_param }
visit edit_admin_namespace_project_path(project_params)

View File

@ -15,8 +15,6 @@ RSpec.describe 'User creates release', :js, feature_category: :continuous_delive
let(:tag_name) { 'new-tag' }
before do
resize_window(1920, 1080)
project.add_developer(user)
sign_in(user)
@ -26,10 +24,6 @@ RSpec.describe 'User creates release', :js, feature_category: :continuous_delive
wait_for_requests
end
after do
restore_window_size
end
it 'renders the breadcrumbs', :aggregate_failures do
within_testid('breadcrumb-links') do
expect(page).to have_content("#{project.creator.name} #{project.name} Releases New release")

View File

@ -12,8 +12,6 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery
let(:release_link) { create(:release_link, release: release) }
before do
resize_window(1920, 1080)
project.add_developer(user)
sign_in(user)
@ -23,10 +21,6 @@ RSpec.describe 'User edits Release', :js, feature_category: :continuous_delivery
wait_for_requests
end
after do
restore_window_size
end
def fill_out_form_and_click(button_to_click)
fill_in 'release-title', with: 'Updated Release title', fill_options: { clear: :backspace }
fill_in 'release-notes', with: 'Updated Release notes'

View File

@ -18,8 +18,6 @@ RSpec.describe 'User views Release', :js, feature_category: :continuous_delivery
end
before do
resize_window(1920, 1080)
project.add_developer(user)
sign_in(user)
@ -27,10 +25,6 @@ RSpec.describe 'User views Release', :js, feature_category: :continuous_delivery
visit project_release_path(project, release)
end
after do
restore_window_size
end
it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
it 'renders the breadcrumbs' do

View File

@ -1,70 +0,0 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import GlobalSearchHeaderApp from '~/super_sidebar/components/global_search/components/global_search_header_app.vue';
import { useMockInternalEventsTracking } from 'helpers/tracking_internal_events_helper';
import waitForPromises from 'helpers/wait_for_promises';
import SearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
jest.mock('~/lib/utils/css_utils', () => ({
isNarrowScreen: jest.fn(),
isNarrowScreenAddListener: jest.fn(),
isNarrowScreenRemoveListener: jest.fn(),
}));
describe('GlobalSearchHeaderApp', () => {
let wrapper;
const createComponent = ({ features = { searchButtonTopRight: true } } = {}) => {
wrapper = shallowMountExtended(GlobalSearchHeaderApp, {
provide: {
glFeatures: {
...features,
},
},
});
};
const findSearchButton = () => wrapper.findByTestId('super-sidebar-search-button');
const findSearchModal = () => wrapper.findComponent(SearchModal);
describe('Render', () => {
const { bindInternalEventDocument } = useMockInternalEventsTracking();
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it('should render search button', () => {
expect(findSearchButton().exists()).toBe(true);
});
it('search button should have tracking', async () => {
const { trackEventSpy } = bindInternalEventDocument(findSearchButton().element);
await findSearchButton().vm.$emit('click');
expect(trackEventSpy).toHaveBeenCalledWith(
'click_search_button_to_activate_command_palette',
{ label: 'top_right' },
undefined,
);
});
it('should render search modal', () => {
expect(findSearchModal().exists()).toBe(true);
});
describe('when feature flag is off', () => {
beforeEach(() => {
createComponent({ features: { searchButtonTopRight: false } });
});
it('should not render search button', () => {
expect(findSearchButton().exists()).toBe(false);
});
it('should not render search modal', () => {
expect(findSearchModal().exists()).toBe(false);
});
});
});
});

View File

@ -252,20 +252,6 @@ describe('UserBar component', () => {
expect(tooltip.value).toBe(`Type <kbd>/</kbd> to search`);
});
});
describe('when feature flag is on', () => {
beforeEach(() => {
createWrapper({ provideOverrides: { glFeatures: { searchButtonTopRight: true } } });
});
it('should not render search button', () => {
expect(findSearchButton().exists()).toBe(false);
});
it('should not render search modal', () => {
expect(findSearchModal().exists()).toBe(false);
});
});
});
describe('While impersonating a user', () => {

View File

@ -53,7 +53,6 @@ describe('IssuableListRoot component', () => {
tabCounts: mockIssuableListProps.tabCounts,
currentTab: mockIssuableListProps.currentTab,
truncateCounts: false,
addPadding: false,
});
});
@ -66,12 +65,6 @@ describe('IssuableListRoot component', () => {
it('renders contents for slot "nav-actions" within IssuableTab component', () => {
expect(findIssuableTabs().find('.js-new-issuable').text()).toBe('New issuable');
});
it('sets "addPadding" prop correctly when updated', async () => {
await wrapper.setProps({ addPadding: true });
expect(findIssuableTabs().props('addPadding')).toBe(true);
});
});
describe('FilteredSearchBar component', () => {

View File

@ -86,14 +86,6 @@ describe('IssuableTabs', () => {
expect(button.text()).toBe('New issuable');
});
it('renders contents for slot "title"', () => {
wrapper = createComponent();
const title = wrapper.find('h1.title');
expect(title.text()).toBe('Tab title slot');
});
});
describe('counts', () => {

View File

@ -0,0 +1,26 @@
import { shallowMount } from '@vue/test-utils';
import WorkItemListHeading from '~/work_items/components/work_item_list_heading.vue';
describe('WorkItemListHeading', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(WorkItemListHeading, {
slots: {
default: '<div id="slot-content">Hello</div>',
},
});
};
it('displays the "Work items" title', () => {
createComponent();
const h1 = wrapper.find('h1');
expect(h1.exists()).toBe(true);
expect(h1.text()).toBe('Work items');
});
it('displays slot content', () => {
createComponent();
expect(wrapper.find('#slot-content').exists()).toBe(true);
});
});

View File

@ -9,6 +9,7 @@ import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_st
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import WorkItemBulkEditSidebar from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue';
import WorkItemHealthStatus from '~/work_items/components/work_item_health_status.vue';
import WorkItemListHeading from '~/work_items/components/work_item_list_heading.vue';
import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional';
@ -103,6 +104,7 @@ describeSkipVue3(skipReason, () => {
const findCreateWorkItemModal = () => wrapper.findComponent(CreateWorkItemModal);
const findBulkEditStartButton = () => wrapper.find('[data-testid="bulk-edit-start-button"]');
const findBulkEditSidebar = () => wrapper.findComponent(WorkItemBulkEditSidebar);
const findWorkItemListHeading = () => wrapper.findComponent(WorkItemListHeading);
const mountComponent = ({
provide = {},
@ -111,6 +113,7 @@ describeSkipVue3(skipReason, () => {
sortPreferenceMutationResponse = mutationHandler,
workItemsViewPreference = false,
workItemsToggleEnabled = true,
workItemPlanningView = false,
props = {},
additionalHandlers = [],
} = {}) => {
@ -133,6 +136,7 @@ describeSkipVue3(skipReason, () => {
provide: {
glFeatures: {
okrsMvc: true,
workItemPlanningView,
},
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
canBulkUpdate: true,
@ -399,10 +403,12 @@ describeSkipVue3(skipReason, () => {
await waitForPromises();
expect(defaultQueryHandler).toHaveBeenCalledTimes(1);
expect(defaultCountsQueryHandler).toHaveBeenCalledTimes(1);
await wrapper.setProps({ eeWorkItemUpdateCount: 1 });
expect(defaultQueryHandler).toHaveBeenCalledTimes(2);
expect(defaultCountsQueryHandler).toHaveBeenCalledTimes(2);
});
});
});
@ -1101,4 +1107,13 @@ describeSkipVue3(skipReason, () => {
expect(findIssuableList().props('showBulkEditSidebar')).toBe(true);
});
});
describe('when workItemPlanningView flag is enabled', () => {
it('renders the WorkItemListHeading component', async () => {
mountComponent({ workItemPlanningView: true });
await waitForPromises();
expect(findWorkItemListHeading().exists()).toBe(true);
});
});
});

View File

@ -185,6 +185,65 @@ RSpec.describe Emails::Imports, feature_category: :importers do
it_behaves_like 'appearance header and footer not enabled'
end
describe '#import_source_user_complete' do
let(:user) { build_stubbed(:user) }
let(:group) { build_stubbed(:group) }
let(:source_user) do
build_stubbed(
:import_source_user, :completed, :with_reassigned_by_user, namespace: group, reassign_to_user: user
)
end
subject { Notify.import_source_user_complete('user_id') }
before do
allow(Import::SourceUser).to receive(:find).and_return(source_user)
end
it 'sends reassignment complete email with reference to group owners for help' do
is_expected.to have_subject("Reassignments in #{group.full_path} completed")
is_expected.to have_content("Imported from: #{source_user.source_hostname}")
is_expected.to have_content("Imported to: #{group.name}")
is_expected.to have_content("Reassigned from: #{source_user.source_name} (@#{source_user.source_username})")
is_expected.to have_content("Reassigned to: #{user.name} (@#{user.username})")
is_expected.to have_content(
"Reassigned by: #{source_user.reassigned_by_user.name} (@#{source_user.reassigned_by_user.username})"
)
is_expected.to have_content("contact #{source_user.reassigned_by_user.name} or another group owner.")
is_expected.to have_link(user.to_reference, href: user_url(user))
is_expected.to have_link(
source_user.reassigned_by_user.to_reference,
href: user_url(source_user.reassigned_by_user)
)
end
context 'when admin placeholder bypass is enabled' do
before do
stub_application_setting(allow_bypass_placeholder_confirmation: true)
end
it 'sends reassignment complete email with reference to admins for help' do
is_expected.to have_subject("Reassignments in #{group.full_path} completed")
is_expected.to have_content("Imported from: #{source_user.source_hostname}")
is_expected.to have_content("Imported to: #{group.name}")
is_expected.to have_content("Reassigned from: #{source_user.source_name} (@#{source_user.source_username})")
is_expected.to have_content("Reassigned to: #{user.name} (@#{user.username})")
is_expected.to have_content(
"Reassigned by: #{source_user.reassigned_by_user.name} (@#{source_user.reassigned_by_user.username})"
)
is_expected.to have_content("contact #{source_user.reassigned_by_user.name} or another administrator.")
is_expected.to have_link(user.to_reference, href: user_url(user))
is_expected.to have_link(
source_user.reassigned_by_user.to_reference,
href: user_url(source_user.reassigned_by_user)
)
end
end
it_behaves_like 'appearance header and footer enabled'
it_behaves_like 'appearance header and footer not enabled'
end
# rubocop:disable RSpec/FactoryBot/AvoidCreate -- creates are required in this case
describe '#project_import_complete' do
let(:user) { create(:user) }

View File

@ -71,6 +71,14 @@ RSpec.describe Import::ValidateRemoteGitEndpointService, feature_category: :impo
end
end
context 'when remote times out' do
before do
allow(Gitlab::GitalyClient::RemoteService).to receive(:exists?).and_raise(GRPC::DeadlineExceeded)
end
include_examples 'error response'
end
context 'with auth credentials' do
let(:user) { 'foo' }
let(:password) { 'bar' }

View File

@ -6,7 +6,7 @@ RSpec.describe Import::DeletePlaceholderUserWorker, feature_category: :importers
let_it_be(:placeholder_user) { create(:user, :placeholder) }
let_it_be(:source_user) { create(:import_source_user, placeholder_user: placeholder_user) }
let(:job_args) { [placeholder_user.id, { type: 'placeholder_user' }] }
let(:job_args) { [placeholder_user.id, { 'type' => 'placeholder_user' }] }
subject(:perform) { described_class.new.perform(*job_args) }
@ -68,7 +68,7 @@ RSpec.describe Import::DeletePlaceholderUserWorker, feature_category: :importers
end
context 'when there is no placeholder user' do
let(:job_args) { [-1, { type: 'placeholder_user' }] }
let(:job_args) { [-1, { 'type' => 'placeholder_user' }] }
it 'does not delete the placeholder_user and does not log an issue' do
expect(::Import::Framework::Logger).not_to receive(:warn)
@ -80,7 +80,7 @@ RSpec.describe Import::DeletePlaceholderUserWorker, feature_category: :importers
context 'when attempting to delete a user who is not a placeholder' do
let_it_be(:user) { create(:user, :import_user) }
let(:job_args) { [user.id, { type: 'placeholder_user' }] }
let(:job_args) { [user.id, { 'type' => 'placeholder_user' }] }
it 'does not delete the user' do
expect(DeleteUserWorker).not_to receive(:perform_async)

View File

@ -53,10 +53,33 @@ RSpec.describe Import::ReassignPlaceholderUserRecordsWorker, feature_category: :
it 'queues a DeletePlaceholderUserWorker with the placeholder user ID' do
expect(Import::DeletePlaceholderUserWorker)
.to receive(:perform_async).with(import_source_user.placeholder_user_id, { type: 'placeholder_user' })
.to receive(:perform_async).with(import_source_user.placeholder_user_id, { 'type' => 'placeholder_user' })
perform_multiple(job_args)
end
it 'does not send the reassigned user an email by default' do
expect(Notify).not_to receive(:import_source_user_complete)
perform_multiple(job_args)
end
context 'when placeholder confirmation is bypassed' do
let(:job_args) { [import_source_user.id, { 'confirmation_skipped' => true }] }
it 'sends an email notification to active reassigned users' do
expect(Notify).to receive_message_chain(:import_source_user_complete, :deliver_later)
perform_multiple(job_args)
end
it 'does not send an email notification to inactive reassigned users' do
import_source_user.reassign_to_user.block!
expect(Notify).not_to receive(:import_source_user_complete)
perform_multiple(job_args)
end
end
end
end

View File

@ -7,6 +7,8 @@ import (
"bufio"
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"net/url"
"os"
@ -72,6 +74,11 @@ func skipUnlessRealGitaly(t *testing.T) {
func realGitalyAuthResponse(gitalyAddress string, apiResponse *api.Response) *api.Response {
apiResponse.GitalyServer.Address = gitalyAddress
// Prevent state of previous tests from interfering with other tests
randomBytes := make([]byte, 8)
rand.Read(randomBytes)
apiResponse.Repository.RelativePath = fmt.Sprintf("foo/bar_%s.git", hex.EncodeToString(randomBytes))
return apiResponse
}
@ -79,6 +86,30 @@ func realGitalyOkBody(t *testing.T, gitalyAddress string) *api.Response {
return realGitalyAuthResponse(gitalyAddress, gitOkBody(t))
}
func removeGitalyRepository(_ *testing.T, apiResponse *api.Response) error {
ctx, repository, err := gitaly.NewRepositoryClient(context.Background(), apiResponse.GitalyServer)
if err != nil {
return err
}
// Remove the repository if it already exists, for consistency
if _, removeRepoErr := repository.RepositoryServiceClient.RemoveRepository(ctx, &gitalypb.RemoveRepositoryRequest{
Repository: &gitalypb.Repository{
StorageName: apiResponse.Repository.StorageName,
RelativePath: apiResponse.Repository.RelativePath,
},
}); removeRepoErr != nil {
status, ok := status.FromError(removeRepoErr)
if !ok || !(status.Code() == codes.NotFound && (status.Message() == "repository does not exist" || status.Message() == "repository not found")) {
return fmt.Errorf("remove repository: %w", removeRepoErr)
}
// Repository didn't exist.
}
return nil
}
func ensureGitalyRepository(_ *testing.T, apiResponse *api.Response) error {
ctx, repository, err := gitaly.NewRepositoryClient(context.Background(), apiResponse.GitalyServer)
if err != nil {
@ -132,6 +163,7 @@ func TestAllowedClone(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
defer removeGitalyRepository(t, apiResponse)
// Prepare test server and backend
ts := testAuthServer(t, nil, nil, 200, apiResponse)
@ -158,6 +190,7 @@ func TestAllowedShallowClone(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
defer removeGitalyRepository(t, apiResponse)
// Prepare test server and backend
ts := testAuthServer(t, nil, nil, 200, apiResponse)
@ -184,6 +217,7 @@ func TestAllowedPush(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
defer removeGitalyRepository(t, apiResponse)
// Prepare the test server and backend
ts := testAuthServer(t, nil, nil, 200, apiResponse)
@ -210,6 +244,7 @@ func TestAllowedGetGitBlob(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
defer removeGitalyRepository(t, apiResponse)
// the LICENSE file in the test repository
oid := "50b27c6518be44c42c4d87966ae2481ce895624c"
@ -250,6 +285,7 @@ func TestAllowedGetGitArchive(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
defer removeGitalyRepository(t, apiResponse)
archivePath := path.Join(t.TempDir(), "my/path")
archivePrefix := repo1
@ -299,6 +335,7 @@ func TestAllowedGetGitArchiveOldPayload(t *testing.T) {
apiResponse := realGitalyOkBody(t, gitalyAddress)
repo := &apiResponse.Repository
require.NoError(t, ensureGitalyRepository(t, apiResponse))
defer removeGitalyRepository(t, apiResponse)
archivePath := path.Join(t.TempDir(), "my/path")
archivePrefix := repo1
@ -349,6 +386,7 @@ func TestAllowedGetGitDiff(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
defer removeGitalyRepository(t, apiResponse)
msg := serializedMessage("RawDiffRequest", &gitalypb.RawDiffRequest{
Repository: &apiResponse.Repository,
@ -379,6 +417,7 @@ func TestAllowedGetGitFormatPatch(t *testing.T) {
// Create the repository in the Gitaly server
apiResponse := realGitalyOkBody(t, gitalyAddress)
require.NoError(t, ensureGitalyRepository(t, apiResponse))
defer removeGitalyRepository(t, apiResponse)
msg := serializedMessage("RawPatchRequest", &gitalypb.RawPatchRequest{
Repository: &apiResponse.Repository,