Add latest changes from gitlab-org/gitlab@master
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
%div{ id: 'js-advanced-search-modal' }
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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) %>
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 36 KiB |
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||