Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
94823a9248
commit
c80b69a93f
|
|
@ -1275,5 +1275,3 @@ FactoryBot/InlineAssociation:
|
|||
- 'spec/factories/packages.rb'
|
||||
- 'spec/factories/packages/package_file.rb'
|
||||
- 'spec/factories/sent_notifications.rb'
|
||||
- 'spec/factories/uploads.rb'
|
||||
- 'spec/factories/wiki_pages.rb'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.28.0
|
||||
1.29.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,211 @@
|
|||
<script>
|
||||
import {
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
GlDropdown,
|
||||
GlDropdownForm,
|
||||
GlDropdownItem,
|
||||
GlSearchBoxByType,
|
||||
GlButton,
|
||||
GlTooltipDirective as GlTooltip,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
GlDropdown,
|
||||
GlDropdownForm,
|
||||
GlDropdownItem,
|
||||
GlSearchBoxByType,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip,
|
||||
},
|
||||
props: {
|
||||
projectsFetchPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dropdownButtonTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
dropdownHeaderTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
moveInProgress: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
projectsListLoading: false,
|
||||
projectsListLoadFailed: false,
|
||||
searchKey: '',
|
||||
projects: [],
|
||||
selectedProject: null,
|
||||
projectItemClick: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasNoSearchResults() {
|
||||
return Boolean(
|
||||
!this.projectsListLoading &&
|
||||
!this.projectsListLoadFailed &&
|
||||
this.searchKey &&
|
||||
!this.projects.length,
|
||||
);
|
||||
},
|
||||
failedToLoadResults() {
|
||||
return !this.projectsListLoading && this.projectsListLoadFailed;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchKey(value = '') {
|
||||
this.fetchProjects(value);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchProjects(search = '') {
|
||||
this.projectsListLoading = true;
|
||||
this.projectsListLoadFailed = false;
|
||||
return axios
|
||||
.get(this.projectsFetchPath, {
|
||||
params: {
|
||||
search,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.projects = data;
|
||||
this.$refs.searchInput.focusInput();
|
||||
})
|
||||
.catch(() => {
|
||||
this.projectsListLoadFailed = true;
|
||||
})
|
||||
.finally(() => {
|
||||
this.projectsListLoading = false;
|
||||
});
|
||||
},
|
||||
isSelectedProject(project) {
|
||||
if (this.selectedProject) {
|
||||
return this.selectedProject.id === project.id;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* This handler is to prevent dropdown
|
||||
* from closing when an item is selected
|
||||
* and emit an event only when dropdown closes.
|
||||
*/
|
||||
handleDropdownHide(e) {
|
||||
if (this.projectItemClick) {
|
||||
e.preventDefault();
|
||||
this.projectItemClick = false;
|
||||
} else {
|
||||
this.$emit('dropdown-close');
|
||||
}
|
||||
},
|
||||
handleDropdownCloseClick() {
|
||||
this.$refs.dropdown.hide();
|
||||
},
|
||||
handleProjectSelect(project) {
|
||||
this.selectedProject = project.id === this.selectedProject?.id ? null : project;
|
||||
this.projectItemClick = true;
|
||||
},
|
||||
handleMoveClick() {
|
||||
this.$refs.dropdown.hide();
|
||||
this.$emit('move-issuable', this.selectedProject);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown">
|
||||
<div
|
||||
v-gl-tooltip.left.viewport
|
||||
data-testid="move-collapsed"
|
||||
:title="dropdownButtonTitle"
|
||||
class="sidebar-collapsed-icon"
|
||||
@click="$emit('toggle-collapse')"
|
||||
>
|
||||
<gl-icon name="arrow-right" />
|
||||
</div>
|
||||
<gl-dropdown
|
||||
ref="dropdown"
|
||||
:block="true"
|
||||
:disabled="moveInProgress"
|
||||
class="hide-collapsed"
|
||||
toggle-class="js-sidebar-dropdown-toggle"
|
||||
@shown="fetchProjects"
|
||||
@hide="handleDropdownHide"
|
||||
>
|
||||
<template #button-content
|
||||
><gl-loading-icon v-if="moveInProgress" class="gl-mr-3" />{{
|
||||
dropdownButtonTitle
|
||||
}}</template
|
||||
>
|
||||
<gl-dropdown-form class="gl-pt-0">
|
||||
<div
|
||||
data-testid="header"
|
||||
class="gl-display-flex gl-pb-3 gl-border-1 gl-border-b-solid gl-border-gray-100"
|
||||
>
|
||||
<span class="gl-flex-grow-1 gl-text-center gl-font-weight-bold gl-py-1">{{
|
||||
dropdownHeaderTitle
|
||||
}}</span>
|
||||
<gl-button
|
||||
variant="link"
|
||||
icon="close"
|
||||
class="gl-mr-2 gl-w-auto! gl-p-2!"
|
||||
@click.prevent="handleDropdownCloseClick"
|
||||
/>
|
||||
</div>
|
||||
<gl-search-box-by-type
|
||||
ref="searchInput"
|
||||
v-model.trim="searchKey"
|
||||
:placeholder="__('Search project')"
|
||||
:debounce="300"
|
||||
/>
|
||||
<div data-testid="content" class="dropdown-content">
|
||||
<gl-loading-icon v-if="projectsListLoading" size="md" class="gl-p-5" />
|
||||
<ul v-else>
|
||||
<gl-dropdown-item
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
:is-check-item="true"
|
||||
:is-checked="isSelectedProject(project)"
|
||||
@click.stop.prevent="handleProjectSelect(project)"
|
||||
>{{ project.name_with_namespace }}</gl-dropdown-item
|
||||
>
|
||||
</ul>
|
||||
<div v-if="hasNoSearchResults" class="gl-text-center gl-p-3">
|
||||
{{ __('No matching results') }}
|
||||
</div>
|
||||
<div v-if="failedToLoadResults" class="gl-text-center gl-p-3">
|
||||
{{ __('Failed to load projects') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-testid="footer"
|
||||
class="gl-pt-3 gl-px-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
|
||||
>
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="success"
|
||||
:disabled="!Boolean(selectedProject)"
|
||||
class="gl-text-center! issuable-move-button"
|
||||
@click="handleMoveClick"
|
||||
>{{ __('Move') }}</gl-button
|
||||
>
|
||||
</div>
|
||||
</gl-dropdown-form>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -921,6 +921,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Following overrides are done to prevent
|
||||
* legacy dropdown styles from influencing
|
||||
* GitLab UI components used within GlDropdown
|
||||
*/
|
||||
.issuable-move-dropdown {
|
||||
.b-dropdown-form {
|
||||
@include gl-p-0;
|
||||
}
|
||||
|
||||
.gl-search-box-by-type button.gl-clear-icon-button:hover {
|
||||
@include gl-bg-transparent;
|
||||
}
|
||||
|
||||
.issuable-move-button:not(.disabled):hover {
|
||||
@include gl-text-white;
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar-collapsed {
|
||||
.sidebar-grouped-item {
|
||||
.sidebar-collapsed-icon {
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@ module Projects
|
|||
def log_response(response)
|
||||
log_data = LOG_DATA_BASE.merge(
|
||||
container_repository_id: @container_repository.id,
|
||||
message: 'deleted tags'
|
||||
)
|
||||
message: 'deleted tags',
|
||||
deleted_tags_count: response[:deleted]&.size
|
||||
).compact
|
||||
|
||||
if response[:status] == :success
|
||||
log_data[:deleted_tags_count] = response[:deleted].size
|
||||
log_info(log_data)
|
||||
else
|
||||
log_data[:message] = response[:message]
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ module Projects
|
|||
def initialize(container_repository, tag_names)
|
||||
@container_repository = container_repository
|
||||
@tag_names = tag_names
|
||||
@deleted_tags = []
|
||||
end
|
||||
|
||||
# Delete tags by name with a single DELETE request. This is only supported
|
||||
|
|
@ -25,7 +26,7 @@ module Projects
|
|||
delete_tags
|
||||
rescue TimeoutError => e
|
||||
::Gitlab::ErrorTracking.track_exception(e, tags_count: @tag_names&.size, container_repository_id: @container_repository&.id)
|
||||
error('timeout while deleting tags')
|
||||
error('timeout while deleting tags', nil, pass_back: { deleted: @deleted_tags })
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -33,13 +34,15 @@ module Projects
|
|||
def delete_tags
|
||||
start_time = Time.zone.now
|
||||
|
||||
deleted_tags = @tag_names.select do |name|
|
||||
@tag_names.each do |name|
|
||||
raise TimeoutError if timeout?(start_time)
|
||||
|
||||
@container_repository.delete_tag_by_name(name)
|
||||
if @container_repository.delete_tag_by_name(name)
|
||||
@deleted_tags.append(name)
|
||||
end
|
||||
end
|
||||
|
||||
deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
|
||||
@deleted_tags.any? ? success(deleted: @deleted_tags) : error('could not delete tags')
|
||||
end
|
||||
|
||||
def timeout?(start_time)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Improving Container Registry Delete Tags Service to log number of successfully
|
||||
deleted tags even if deletion process was interrupted by a timeout
|
||||
merge_request: 46079
|
||||
author: Maksim Stankevic, @maksimstankevic
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update diff_max_patch_bytes from 100kb -> 200kb
|
||||
merge_request: 46276
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Upgrade GitLab Pages to 1.29.0
|
||||
merge_request: 46665
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class IncreaseDefaultDiffMaxPatchBytes < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
change_column_default(:application_settings, :diff_max_patch_bytes, from: 102400, to: 204800)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MigrateDefaultDiffMaxPatchBytesToMinimum200kb < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
MAX_SIZE = 200.kilobytes
|
||||
|
||||
class ApplicationSetting < ActiveRecord::Base
|
||||
self.table_name = 'application_settings'
|
||||
end
|
||||
|
||||
def up
|
||||
table = ApplicationSetting.arel_table
|
||||
ApplicationSetting.where(table[:diff_max_patch_bytes].lt(MAX_SIZE)).update_all(diff_max_patch_bytes: MAX_SIZE)
|
||||
end
|
||||
|
||||
def down
|
||||
table = ApplicationSetting.arel_table
|
||||
ApplicationSetting.where(table[:diff_max_patch_bytes].eq(MAX_SIZE)).update_all(diff_max_patch_bytes: 100.kilobytes)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
402e9a6e92802888ba01ee216850ab5b0fe9997a84415c9ffe8d5d37a9823220
|
||||
|
|
@ -0,0 +1 @@
|
|||
ec5bab20a1b591b77b48b85dc0b871e88a41891d256201b7d8eb86195ef1c4ad
|
||||
|
|
@ -9207,7 +9207,7 @@ CREATE TABLE application_settings (
|
|||
custom_project_templates_group_id integer,
|
||||
usage_stats_set_by_user_id integer,
|
||||
receive_max_input_size integer,
|
||||
diff_max_patch_bytes integer DEFAULT 102400 NOT NULL,
|
||||
diff_max_patch_bytes integer DEFAULT 204800 NOT NULL,
|
||||
archive_builds_in_seconds integer,
|
||||
commit_email_hostname character varying,
|
||||
protected_ci_variables boolean DEFAULT true NOT NULL,
|
||||
|
|
@ -12749,10 +12749,10 @@ CREATE TABLE group_wiki_repositories (
|
|||
|
||||
CREATE TABLE historical_data (
|
||||
id integer NOT NULL,
|
||||
date date,
|
||||
active_user_count integer,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
date date,
|
||||
recorded_at timestamp with time zone,
|
||||
CONSTRAINT check_640e8cf66c CHECK ((recorded_at IS NOT NULL))
|
||||
);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ module Gitlab
|
|||
#
|
||||
# If this value ever changes, make sure to create a migration to update
|
||||
# current records, and default of `ApplicationSettings#diff_max_patch_bytes`.
|
||||
DEFAULT_MAX_PATCH_BYTES = 100.kilobytes
|
||||
DEFAULT_MAX_PATCH_BYTES = 200.kilobytes
|
||||
|
||||
# This is a limitation applied on the source (Gitaly), therefore we don't allow
|
||||
# persisting limits over that.
|
||||
|
|
|
|||
|
|
@ -11139,6 +11139,9 @@ msgstr ""
|
|||
msgid "Failed to load milestones. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to load related branches"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.5",
|
||||
"@gitlab/svgs": "1.174.0",
|
||||
"@gitlab/ui": "21.41.0",
|
||||
"@gitlab/ui": "21.42.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "^6.0.3-3",
|
||||
"@rails/ujs": "^6.0.3-2",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
FactoryBot.define do
|
||||
factory :upload do
|
||||
model { create(:project) }
|
||||
model { association(:project) }
|
||||
size { 100.kilobytes }
|
||||
uploader { "AvatarUploader" }
|
||||
mount_point { :avatar }
|
||||
|
|
@ -20,7 +20,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :personal_snippet_upload do
|
||||
model { create(:personal_snippet) }
|
||||
model { association(:personal_snippet) }
|
||||
path { File.join(secret, filename) }
|
||||
uploader { "PersonalFileUploader" }
|
||||
secret { SecureRandom.hex }
|
||||
|
|
@ -46,7 +46,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :namespace_upload do
|
||||
model { create(:group) }
|
||||
model { association(:group) }
|
||||
path { File.join(secret, filename) }
|
||||
uploader { "NamespaceFileUploader" }
|
||||
secret { SecureRandom.hex }
|
||||
|
|
@ -54,7 +54,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :favicon_upload do
|
||||
model { create(:appearance) }
|
||||
model { association(:appearance) }
|
||||
uploader { "FaviconUploader" }
|
||||
secret { SecureRandom.hex }
|
||||
mount_point { :favicon }
|
||||
|
|
@ -62,13 +62,13 @@ FactoryBot.define do
|
|||
|
||||
trait :attachment_upload do
|
||||
mount_point { :attachment }
|
||||
model { create(:note) }
|
||||
model { association(:note) }
|
||||
uploader { "AttachmentUploader" }
|
||||
end
|
||||
|
||||
trait :design_action_image_v432x230_upload do
|
||||
mount_point { :image_v432x230 }
|
||||
model { create(:design_action) }
|
||||
model { association(:design_action) }
|
||||
uploader { ::DesignManagement::DesignV432x230Uploader.name }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -39,14 +39,14 @@ FactoryBot.define do
|
|||
|
||||
factory :wiki_page_meta, class: 'WikiPage::Meta' do
|
||||
title { generate(:wiki_page_title) }
|
||||
project { create(:project) }
|
||||
project { association(:project) }
|
||||
|
||||
trait :for_wiki_page do
|
||||
transient do
|
||||
wiki_page { create(:wiki_page, container: project) }
|
||||
wiki_page { association(:wiki_page, container: project) }
|
||||
end
|
||||
|
||||
project { @overrides[:wiki_page]&.container || create(:project) }
|
||||
project { @overrides[:wiki_page]&.container || association(:project) }
|
||||
title { wiki_page.title }
|
||||
|
||||
initialize_with do
|
||||
|
|
@ -58,7 +58,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
factory :wiki_page_slug, class: 'WikiPage::Slug' do
|
||||
wiki_page_meta { create(:wiki_page_meta) }
|
||||
wiki_page_meta { association(:wiki_page_meta) }
|
||||
slug { generate(:sluggified_title) }
|
||||
canonical { false }
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ RSpec.describe 'Expand and collapse diffs', :js do
|
|||
|
||||
before do
|
||||
stub_feature_flags(increased_diff_limits: false)
|
||||
allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(100.kilobytes)
|
||||
|
||||
sign_in(create(:admin))
|
||||
|
||||
# Ensure that undiffable.md is in .gitattributes
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ RSpec.describe 'User expands diff', :js do
|
|||
|
||||
before do
|
||||
stub_feature_flags(increased_diff_limits: false)
|
||||
allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(100.kilobytes)
|
||||
|
||||
visit(diffs_project_merge_request_path(project, merge_request))
|
||||
|
||||
wait_for_requests
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ exports[`Design management upload button component renders loading icon 1`] = `
|
|||
|
||||
<gl-loading-icon-stub
|
||||
class="ml-1"
|
||||
color="orange"
|
||||
color="dark"
|
||||
inline="true"
|
||||
label="Loading"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ exports[`Design management index page designs renders loading icon 1`] = `
|
|||
class="gl-mt-6"
|
||||
>
|
||||
<gl-loading-icon-stub
|
||||
color="orange"
|
||||
color="dark"
|
||||
label="Loading"
|
||||
size="md"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ exports[`Design management design index page sets loading state 1`] = `
|
|||
>
|
||||
<gl-loading-icon-stub
|
||||
class="gl-align-self-center"
|
||||
color="orange"
|
||||
color="dark"
|
||||
label="Loading"
|
||||
size="xl"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ exports[`SidebarTodo template renders component container element with proper da
|
|||
</span>
|
||||
|
||||
<gl-loading-icon-stub
|
||||
color="orange"
|
||||
color="dark"
|
||||
inline="true"
|
||||
label="Loading"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,375 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import {
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
GlDropdown,
|
||||
GlDropdownForm,
|
||||
GlDropdownItem,
|
||||
GlSearchBoxByType,
|
||||
GlButton,
|
||||
} from '@gitlab/ui';
|
||||
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
|
||||
|
||||
const mockProjects = [
|
||||
{
|
||||
id: 2,
|
||||
name_with_namespace: 'Gitlab Org / Gitlab Shell',
|
||||
full_path: 'gitlab-org/gitlab-shell',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name_with_namespace: 'Gnuwget / Wget2',
|
||||
full_path: 'gnuwget/wget2',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name_with_namespace: 'Commit451 / Lab Coat',
|
||||
full_path: 'Commit451/lab-coat',
|
||||
},
|
||||
];
|
||||
|
||||
const mockProps = {
|
||||
projectsFetchPath: '/-/autocomplete/projects?project_id=1',
|
||||
dropdownButtonTitle: 'Move issuable',
|
||||
dropdownHeaderTitle: 'Move issuable',
|
||||
moveInProgress: false,
|
||||
};
|
||||
|
||||
const mockEvent = {
|
||||
stopPropagation: jest.fn(),
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
|
||||
const createComponent = (propsData = mockProps) =>
|
||||
shallowMount(IssuableMoveDropdown, {
|
||||
propsData,
|
||||
});
|
||||
|
||||
describe('IssuableMoveDropdown', () => {
|
||||
let mock;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
wrapper = createComponent();
|
||||
wrapper.vm.$refs.dropdown.hide = jest.fn();
|
||||
wrapper.vm.$refs.searchInput.focusInput = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('watch', () => {
|
||||
describe('searchKey', () => {
|
||||
it('calls `fetchProjects` with value of the prop', async () => {
|
||||
jest.spyOn(wrapper.vm, 'fetchProjects');
|
||||
wrapper.setData({
|
||||
searchKey: 'foo',
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.fetchProjects).toHaveBeenCalledWith('foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
describe('fetchProjects', () => {
|
||||
it('sets projectsListLoading to true and projectsListLoadFailed to false', () => {
|
||||
wrapper.vm.fetchProjects();
|
||||
|
||||
expect(wrapper.vm.projectsListLoading).toBe(true);
|
||||
expect(wrapper.vm.projectsListLoadFailed).toBe(false);
|
||||
});
|
||||
|
||||
it('calls `axios.get` with `projectsFetchPath` and query param `search`', () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: mockProjects,
|
||||
});
|
||||
|
||||
wrapper.vm.fetchProjects('foo');
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(
|
||||
mockProps.projectsFetchPath,
|
||||
expect.objectContaining({
|
||||
params: {
|
||||
search: 'foo',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('sets response to `projects` and focuses on searchInput when request is successful', async () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: mockProjects,
|
||||
});
|
||||
|
||||
await wrapper.vm.fetchProjects('foo');
|
||||
|
||||
expect(wrapper.vm.projects).toBe(mockProjects);
|
||||
expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets projectsListLoadFailed to true when request fails', async () => {
|
||||
jest.spyOn(axios, 'get').mockRejectedValue({});
|
||||
|
||||
await wrapper.vm.fetchProjects('foo');
|
||||
|
||||
expect(wrapper.vm.projectsListLoadFailed).toBe(true);
|
||||
});
|
||||
|
||||
it('sets projectsListLoading to false when request completes', async () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: mockProjects,
|
||||
});
|
||||
|
||||
await wrapper.vm.fetchProjects('foo');
|
||||
|
||||
expect(wrapper.vm.projectsListLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSelectedProject', () => {
|
||||
it.each`
|
||||
project | selectedProject | title | returnValue
|
||||
${mockProjects[0]} | ${mockProjects[0]} | ${'are same projects'} | ${true}
|
||||
${mockProjects[0]} | ${mockProjects[1]} | ${'are different projects'} | ${false}
|
||||
`(
|
||||
'returns $returnValue when selectedProject and provided project param $title',
|
||||
async ({ project, selectedProject, returnValue }) => {
|
||||
wrapper.setData({
|
||||
selectedProject,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.isSelectedProject(project)).toBe(returnValue);
|
||||
},
|
||||
);
|
||||
|
||||
it('returns false when selectedProject is null', async () => {
|
||||
wrapper.setData({
|
||||
selectedProject: null,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.vm.isSelectedProject(mockProjects[0])).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
const findDropdownEl = () => wrapper.find(GlDropdown);
|
||||
|
||||
it('renders collapsed state element with icon', () => {
|
||||
const collapsedEl = wrapper.find('[data-testid="move-collapsed"]');
|
||||
|
||||
expect(collapsedEl.exists()).toBe(true);
|
||||
expect(collapsedEl.attributes('title')).toBe(mockProps.dropdownButtonTitle);
|
||||
expect(collapsedEl.find(GlIcon).exists()).toBe(true);
|
||||
expect(collapsedEl.find(GlIcon).props('name')).toBe('arrow-right');
|
||||
});
|
||||
|
||||
describe('gl-dropdown component', () => {
|
||||
it('renders component container element', () => {
|
||||
expect(findDropdownEl().exists()).toBe(true);
|
||||
expect(findDropdownEl().props('block')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders gl-dropdown-form component', () => {
|
||||
expect(
|
||||
findDropdownEl()
|
||||
.find(GlDropdownForm)
|
||||
.exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('renders header element', () => {
|
||||
const headerEl = findDropdownEl().find('[data-testid="header"]');
|
||||
|
||||
expect(headerEl.exists()).toBe(true);
|
||||
expect(headerEl.find('span').text()).toBe(mockProps.dropdownHeaderTitle);
|
||||
expect(headerEl.find(GlButton).props('icon')).toBe('close');
|
||||
});
|
||||
|
||||
it('renders gl-search-box-by-type component', () => {
|
||||
const searchEl = findDropdownEl().find(GlSearchBoxByType);
|
||||
|
||||
expect(searchEl.exists()).toBe(true);
|
||||
expect(searchEl.attributes()).toMatchObject({
|
||||
placeholder: 'Search project',
|
||||
debounce: '300',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders gl-loading-icon component when projectsListLoading prop is true', async () => {
|
||||
wrapper.setData({
|
||||
projectsListLoading: true,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(
|
||||
findDropdownEl()
|
||||
.find(GlLoadingIcon)
|
||||
.exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('renders gl-dropdown-item components for available projects', async () => {
|
||||
wrapper.setData({
|
||||
projects: mockProjects,
|
||||
selectedProject: mockProjects[0],
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const dropdownItems = wrapper.findAll(GlDropdownItem);
|
||||
|
||||
expect(dropdownItems).toHaveLength(mockProjects.length);
|
||||
expect(dropdownItems.at(0).props()).toMatchObject({
|
||||
isCheckItem: true,
|
||||
isChecked: true,
|
||||
});
|
||||
expect(dropdownItems.at(0).text()).toBe(mockProjects[0].name_with_namespace);
|
||||
});
|
||||
|
||||
it('renders string "No matching results" when search does not yield any matches', async () => {
|
||||
wrapper.setData({
|
||||
searchKey: 'foo',
|
||||
});
|
||||
|
||||
// Wait for `searchKey` watcher to run.
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper.setData({
|
||||
projects: [],
|
||||
projectsListLoading: false,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const dropdownContentEl = wrapper.find('[data-testid="content"]');
|
||||
|
||||
expect(dropdownContentEl.text()).toContain('No matching results');
|
||||
});
|
||||
|
||||
it('renders string "Failed to load projects" when loading projects list fails', async () => {
|
||||
wrapper.setData({
|
||||
projects: [],
|
||||
projectsListLoading: false,
|
||||
projectsListLoadFailed: true,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const dropdownContentEl = wrapper.find('[data-testid="content"]');
|
||||
|
||||
expect(dropdownContentEl.text()).toContain('Failed to load projects');
|
||||
});
|
||||
|
||||
it('renders gl-button within footer', async () => {
|
||||
const moveButtonEl = wrapper.find('[data-testid="footer"]').find(GlButton);
|
||||
|
||||
expect(moveButtonEl.text()).toBe('Move');
|
||||
expect(moveButtonEl.attributes('disabled')).toBe('true');
|
||||
|
||||
wrapper.setData({
|
||||
selectedProject: mockProjects[0],
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-testid="footer"]')
|
||||
.find(GlButton)
|
||||
.attributes('disabled'),
|
||||
).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('collapsed state element emits `toggle-collapse` event on component when clicked', () => {
|
||||
wrapper.find('[data-testid="move-collapsed"]').trigger('click');
|
||||
|
||||
expect(wrapper.emitted('toggle-collapse')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('gl-dropdown component calls `fetchProjects` on `shown` event', () => {
|
||||
jest.spyOn(axios, 'get').mockResolvedValue({
|
||||
data: mockProjects,
|
||||
});
|
||||
|
||||
findDropdownEl().vm.$emit('shown');
|
||||
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('gl-dropdown component prevents dropdown body from closing on `hide` event when `projectItemClick` prop is true', async () => {
|
||||
wrapper.setData({
|
||||
projectItemClick: true,
|
||||
});
|
||||
|
||||
findDropdownEl().vm.$emit('hide', mockEvent);
|
||||
|
||||
expect(mockEvent.preventDefault).toHaveBeenCalled();
|
||||
expect(wrapper.vm.projectItemClick).toBe(false);
|
||||
});
|
||||
|
||||
it('gl-dropdown component emits `dropdown-close` event on component from `hide` event', async () => {
|
||||
findDropdownEl().vm.$emit('hide');
|
||||
|
||||
expect(wrapper.emitted('dropdown-close')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('close icon in dropdown header closes the dropdown when clicked', () => {
|
||||
wrapper
|
||||
.find('[data-testid="header"]')
|
||||
.find(GlButton)
|
||||
.vm.$emit('click', mockEvent);
|
||||
|
||||
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets project for clicked gl-dropdown-item to selectedProject', async () => {
|
||||
wrapper.setData({
|
||||
projects: mockProjects,
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper
|
||||
.findAll(GlDropdownItem)
|
||||
.at(0)
|
||||
.vm.$emit('click', mockEvent);
|
||||
|
||||
expect(wrapper.vm.selectedProject).toBe(mockProjects[0]);
|
||||
});
|
||||
|
||||
it('hides dropdown and emits `move-issuable` event when move button is clicked', async () => {
|
||||
wrapper.setData({
|
||||
selectedProject: mockProjects[0],
|
||||
});
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
wrapper
|
||||
.find('[data-testid="footer"]')
|
||||
.find(GlButton)
|
||||
.vm.$emit('click');
|
||||
|
||||
expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalled();
|
||||
expect(wrapper.emitted('move-issuable')).toBeTruthy();
|
||||
expect(wrapper.emitted('move-issuable')[0]).toEqual([mockProjects[0]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -114,7 +114,7 @@ exports[`WebIDE runs 1`] = `
|
|||
>
|
||||
<span
|
||||
aria-label="Loading"
|
||||
class="align-text-bottom gl-spinner gl-spinner-orange gl-spinner-md"
|
||||
class="align-text-bottom gl-spinner gl-spinner-dark gl-spinner-md"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
|
|||
safe_max_files: 100,
|
||||
safe_max_lines: 5000,
|
||||
safe_max_bytes: 512000,
|
||||
max_patch_bytes: 102400
|
||||
max_patch_bytes: 204800
|
||||
)
|
||||
|
||||
expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
|
||||
|
|
@ -57,7 +57,7 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
|
|||
safe_max_files: 100,
|
||||
safe_max_lines: 5000,
|
||||
safe_max_bytes: 512000,
|
||||
max_patch_bytes: 102400
|
||||
max_patch_bytes: 204800
|
||||
)
|
||||
|
||||
expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ RSpec.describe API::Settings, 'Settings' do
|
|||
enforce_terms: true,
|
||||
terms: 'Hello world!',
|
||||
performance_bar_allowed_group_path: group.full_path,
|
||||
diff_max_patch_bytes: 150_000,
|
||||
diff_max_patch_bytes: 300_000,
|
||||
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
|
||||
local_markdown_version: 3,
|
||||
allow_local_requests_from_web_hooks_and_services: true,
|
||||
|
|
@ -148,7 +148,7 @@ RSpec.describe API::Settings, 'Settings' do
|
|||
expect(json_response['enforce_terms']).to be(true)
|
||||
expect(json_response['terms']).to eq('Hello world!')
|
||||
expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
|
||||
expect(json_response['diff_max_patch_bytes']).to eq(150_000)
|
||||
expect(json_response['diff_max_patch_bytes']).to eq(300_000)
|
||||
expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
|
||||
expect(json_response['local_markdown_version']).to eq(3)
|
||||
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)
|
||||
|
|
|
|||
|
|
@ -27,13 +27,17 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'logging an error response' do |message: 'could not delete tags'|
|
||||
RSpec.shared_examples 'logging an error response' do |message: 'could not delete tags', extra_log: {}|
|
||||
it 'logs an error message' do
|
||||
expect(service).to receive(:log_error).with(
|
||||
service_class: 'Projects::ContainerRepository::DeleteTagsService',
|
||||
message: message,
|
||||
container_repository_id: repository.id
|
||||
)
|
||||
log_data = {
|
||||
service_class: 'Projects::ContainerRepository::DeleteTagsService',
|
||||
message: message,
|
||||
container_repository_id: repository.id
|
||||
}
|
||||
|
||||
log_data.merge!(extra_log) if extra_log.any?
|
||||
|
||||
expect(service).to receive(:log_error).with(log_data)
|
||||
|
||||
subject
|
||||
end
|
||||
|
|
@ -115,7 +119,7 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
|
|||
|
||||
it { is_expected.to include(status: :error, message: 'timeout while deleting tags') }
|
||||
|
||||
it_behaves_like 'logging an error response', message: 'timeout while deleting tags'
|
||||
it_behaves_like 'logging an error response', message: 'timeout while deleting tags', extra_log: { deleted_tags_count: 0 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do
|
|||
stub_delete_reference_requests('A' => 200)
|
||||
end
|
||||
|
||||
it { is_expected.to include(status: :error, message: 'timeout while deleting tags') }
|
||||
it { is_expected.to eq(status: :error, message: 'timeout while deleting tags', deleted: ['A']) }
|
||||
|
||||
it 'tracks the exception' do
|
||||
expect(::Gitlab::ErrorTracking)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ RSpec.shared_examples 'creates an alert management alert' do
|
|||
end
|
||||
|
||||
it 'executes the alert service hooks' do
|
||||
slack_service = create(:service, type: 'SlackService', project: project, alert_events: true, active: true)
|
||||
expect_next_instance_of(AlertManagement::Alert) do |alert|
|
||||
expect(alert).to receive(:execute_services)
|
||||
end
|
||||
|
||||
subject
|
||||
|
||||
expect(ProjectServiceWorker).to have_received(:perform_async).with(slack_service.id, an_instance_of(Hash))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -866,10 +866,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.174.0.tgz#954b4d908a6188a2fcc45f00f748beeb23f054b0"
|
||||
integrity sha512-CgnZvO2miZkWxANhFdaK+2S4qRgkrMRE3vh3Xxwc+hIV9ki9KavlAAez9MNIs0Um/SJ1UpfmqKoM/dMyZX7K/w==
|
||||
|
||||
"@gitlab/ui@21.41.0":
|
||||
version "21.41.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.41.0.tgz#185f5a534d6cd038f48588f432a25576d08c5780"
|
||||
integrity sha512-Jl0OcEMQ+GKB9wTnZH9rU6YmL3rVDMHkEZc7Sa5QvNvP6A1Se/UEKbzhLi9rdyAoKpvrm3++tYg3ZJklIRZIsg==
|
||||
"@gitlab/ui@21.42.0":
|
||||
version "21.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.42.0.tgz#9cba612a6b0c8ee533865fa0c1e12243ced0c6e2"
|
||||
integrity sha512-1q55KuGozOZ3iQPzE+GD7ChX+BCCe9eYGtaLMrFP6mjtyGDI1v9AHfYqHIqgS3+chaTKWCr8YpJ0PECPaLLM6A==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
"@gitlab/vue-toasted" "^1.3.0"
|
||||
|
|
|
|||
Loading…
Reference in New Issue