Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-12 00:12:46 +00:00
parent f8dfaa8d41
commit 06071708b3
32 changed files with 132 additions and 142 deletions

View file

@ -1,5 +1,5 @@
<script>
import { pickBy, isEmpty } from 'lodash';
import { pickBy, isEmpty, mapValues } from 'lodash';
import { mapActions } from 'vuex';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
@ -251,7 +251,8 @@ export default {
);
}
return {
return mapValues(
{
...notParams,
author_username: authorUsername,
'label_name[]': labelName,
@ -266,7 +267,20 @@ export default {
my_reaction_emoji: myReactionEmoji,
release_tag: releaseTag,
confidential,
};
},
(value) => {
if (value || value === false) {
// note: need to check array for labels.
if (Array.isArray(value)) {
return value.map((valueItem) => encodeURIComponent(valueItem));
}
return encodeURIComponent(value);
}
return value;
},
);
},
},
created() {

View file

@ -20,8 +20,8 @@ export default {
NamespaceSelect,
},
props: {
parentGroups: {
type: Object,
groupNamespaces: {
type: Array,
required: true,
},
isPaidGroup: {
@ -60,7 +60,7 @@ export default {
<gl-form-group v-if="!isPaidGroup">
<namespace-select
:default-text="$options.i18n.dropdownTitle"
:data="parentGroups"
:group-namespaces="groupNamespaces"
:empty-namespace-title="$options.i18n.emptyNamespaceTitle"
:include-headers="false"
include-empty-namespace

View file

@ -5,15 +5,13 @@ import TransferGroupForm, { i18n } from './components/transfer_group_form.vue';
const prepareGroups = (rawGroups) => {
if (!rawGroups) {
return { group: [] };
return [];
}
const group = JSON.parse(rawGroups).map(({ id, text: humanName }) => ({
return JSON.parse(rawGroups).map(({ id, text: humanName }) => ({
id,
humanName,
}));
return { group };
};
export default () => {
@ -38,7 +36,7 @@ export default () => {
render(createElement) {
return createElement(TransferGroupForm, {
props: {
parentGroups: prepareGroups(parentGroups),
groupNamespaces: prepareGroups(parentGroups),
isPaidGroup: parseBoolean(isPaidGroup),
confirmButtonText,
confirmationPhrase: groupName,

View file

@ -11,8 +11,12 @@ export default {
ConfirmDanger,
},
props: {
namespaces: {
type: Object,
groupNamespaces: {
type: Array,
required: true,
},
userNamespaces: {
type: Array,
required: true,
},
confirmationPhrase: {
@ -46,7 +50,8 @@ export default {
<namespace-select
data-testid="transfer-project-namespace"
:full-width="true"
:data="namespaces"
:group-namespaces="groupNamespaces"
:user-namespaces="userNamespaces"
:selected-namespace="selectedNamespace"
@select="handleSelect"
/>

View file

@ -3,10 +3,14 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import TransferProjectForm from './components/transfer_project_form.vue';
const prepareNamespaces = (rawNamespaces = '') => {
if (!rawNamespaces) {
return { groupNamespaces: [], userNamespaces: [] };
}
const data = JSON.parse(rawNamespaces);
return {
group: data?.group.map(convertObjectPropsToCamelCase),
user: data?.user.map(convertObjectPropsToCamelCase),
groupNamespaces: data?.group?.map(convertObjectPropsToCamelCase) || [],
userNamespaces: data?.user?.map(convertObjectPropsToCamelCase) || [],
};
};
@ -35,7 +39,7 @@ export default () => {
props: {
confirmButtonText,
confirmationPhrase,
namespaces: prepareNamespaces(namespaces),
...prepareNamespaces(namespaces),
},
on: {
selectNamespace: (id) => {

View file

@ -34,9 +34,15 @@ export default {
GlSearchBoxByType,
},
props: {
data: {
type: Object,
required: true,
groupNamespaces: {
type: Array,
required: false,
default: () => [],
},
userNamespaces: {
type: Array,
required: false,
default: () => [],
},
fullWidth: {
type: Boolean,
@ -72,18 +78,18 @@ export default {
},
computed: {
hasUserNamespaces() {
return this.data.user?.length;
return this.userNamespaces.length;
},
hasGroupNamespaces() {
return this.data.group?.length;
return this.groupNamespaces.length;
},
filteredGroupNamespaces() {
if (!this.hasGroupNamespaces) return [];
return filterByName(this.data.group, this.searchTerm);
return filterByName(this.groupNamespaces, this.searchTerm);
},
filteredUserNamespaces() {
if (!this.hasUserNamespaces) return [];
return filterByName(this.data.user, this.searchTerm);
return filterByName(this.userNamespaces, this.searchTerm);
},
selectedNamespaceText() {
return this.selectedNamespace?.humanName || this.defaultText;

View file

@ -25,14 +25,20 @@ export default {
workItemTypes() {
return this.workItemHierarchy.reduce(
(itemTypes, item) => {
const skipItem = workItemTypes[item.type].isWorkItem && !window.gon?.features?.workItems;
if (skipItem) {
return itemTypes;
}
const key = item.available ? 'available' : 'unavailable';
const nestedTypes = item.nestedTypes?.map((type) => workItemTypes[type]);
itemTypes[key].push({
...item,
...workItemTypes[item.type],
nestedTypes: item.nestedTypes
? item.nestedTypes.map((type) => workItemTypes[type])
: null,
nestedTypes,
});
return itemTypes;
},
{ available: [], unavailable: [] },

View file

@ -31,6 +31,7 @@ export const workItemTypes = {
icon: 'task-done',
color: '#217645',
backgroundColor: '#C3E6CD',
isWorkItem: true,
},
INCIDENT: {
title: __('Incident'),

View file

@ -370,7 +370,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def check_ban_user_feature_flag
access_denied! unless Feature.enabled?(:ban_user_feature_flag)
access_denied! unless Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml)
end
def log_impersonation_event

View file

@ -7,8 +7,6 @@ module PlanningHierarchy
def planning_hierarchy
return access_denied! unless can?(current_user, :read_planning_hierarchy, @project)
return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml)
render 'shared/planning_hierarchy'
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables

View file

@ -42,6 +42,7 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml)
push_frontend_feature_flag(:work_items, @project, default_enabled: :yaml)
end
layout :determine_layout

View file

@ -169,7 +169,7 @@ module Types
end
def hidden?
object.hidden? if Feature.enabled?(:ban_user_feature_flag)
object.hidden? if Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml)
end
def escalation_status

View file

@ -63,7 +63,7 @@ module IssuesHelper
end
def issue_hidden?(issue)
Feature.enabled?(:ban_user_feature_flag) && issue.hidden?
Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml) && issue.hidden?
end
def hidden_issue_icon(issue)

View file

@ -125,7 +125,7 @@ module UsersHelper
end
def ban_feature_available?
Feature.enabled?(:ban_user_feature_flag)
Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml)
end
def confirm_user_data(user)

View file

@ -143,7 +143,7 @@ class Issue < ApplicationRecord
scope :confidential_only, -> { where(confidential: true) }
scope :without_hidden, -> {
if Feature.enabled?(:ban_user_feature_flag)
if Feature.enabled?(:ban_user_feature_flag, default_enabled: :yaml)
where('NOT EXISTS (?)', Users::BannedUser.select(1).where('issues.author_id = banned_users.user_id'))
else
all

View file

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330667
milestone: '13.12'
type: development
group: group::access
default_enabled: false
default_enabled: true

View file

@ -1,8 +0,0 @@
---
name: work_items_hierarchy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79315
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451
milestone: '14.8'
type: development
group: group::product planning
default_enabled: false

View file

@ -208,7 +208,13 @@ Users can also be activated using the [GitLab API](../../api/users.md#activate-u
## Ban and unban users
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327353) in GitLab 14.2.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327353) in GitLab 14.2 [with a flag](../../administration/feature_flags.md) named `ban_user_feature_flag`. Disabled by default.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/330667) in GitLab 14.8.
FLAG:
On self-managed GitLab, by default this feature is available.
To hide the feature, ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `ban_user_feature_flag`.
On GitLab.com, this feature is available to GitLab.com administrators only.
GitLab administrators can ban and unban users. Banned users are blocked, and their issues are hidden.
The banned user's comments are still displayed. Hiding a banned user's comments is [tracked in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/327356).

View file

@ -57,7 +57,7 @@ Example request using a personal access token:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" \
--upload-file path/to/file.txt \
"https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt?status=hidden"
"https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt"
```
Example response without attribute `select`:

View file

@ -19,7 +19,6 @@ module Sidebars
end
def show_hierarachy_menu_item?(container)
Feature.enabled?(:work_items_hierarchy, container, default_enabled: :yaml) &&
can?(context.current_user, :read_planning_hierarchy, container)
end
end

View file

@ -120,7 +120,7 @@ describe('BoardFilteredSearch', () => {
{ type: 'author', value: { data: 'root', operator: '=' } },
{ type: 'assignee', value: { data: 'root', operator: '=' } },
{ type: 'label', value: { data: 'label', operator: '=' } },
{ type: 'label', value: { data: 'label2', operator: '=' } },
{ type: 'label', value: { data: 'label&2', operator: '=' } },
{ type: 'milestone', value: { data: 'New Milestone', operator: '=' } },
{ type: 'type', value: { data: 'INCIDENT', operator: '=' } },
{ type: 'weight', value: { data: '2', operator: '=' } },
@ -134,7 +134,7 @@ describe('BoardFilteredSearch', () => {
title: '',
replace: true,
url:
'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2&assignee_username=root&milestone_title=New+Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
'http://test.host/?author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=3341&types=INCIDENT&weight=2&release_tag=v1.0.0',
});
});

View file

@ -74,7 +74,7 @@ describe('Dropdown User', () => {
});
describe('hideCurrentUser', () => {
const fixtureTemplate = 'issues/issue_list.html';
const fixtureTemplate = 'merge_requests/merge_request_list.html';
let dropdown;
let authorFilterDropdownElement;

View file

@ -4,7 +4,7 @@ import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dro
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
describe('Dropdown Utils', () => {
const issueListFixture = 'issues/issue_list.html';
const issuableListFixture = 'merge_requests/merge_request_list.html';
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
@ -350,7 +350,7 @@ describe('Dropdown Utils', () => {
let authorToken;
beforeEach(() => {
loadFixtures(issueListFixture);
loadFixtures(issuableListFixture);
authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '=', '@user');
const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');

View file

@ -37,17 +37,6 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
render_issue(create(:closed_issue, project: project))
end
it 'issues/issue_list.html' do
create(:issue, project: project)
get :index, params: {
namespace_id: project.namespace.to_param,
project_id: project
}
expect(response).to be_successful
end
private
def render_issue(issue)

View file

@ -119,6 +119,17 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
end
end
it 'merge_requests/merge_request_list.html' do
create(:merge_request, source_project: project, target_project: project)
get :index, params: {
namespace_id: project.namespace.to_param,
project_id: project
}
expect(response).to be_successful
end
private
def render_discussions_json(merge_request)

View file

@ -10,7 +10,7 @@ describe('Transfer group form', () => {
const confirmButtonText = 'confirm';
const confirmationPhrase = 'confirmation-phrase';
const paidGroupHelpLink = 'some/fake/link';
const groups = [
const groupNamespaces = [
{
id: 1,
humanName: 'Group 1',
@ -22,7 +22,7 @@ describe('Transfer group form', () => {
];
const defaultProps = {
parentGroups: { groups },
groupNamespaces,
paidGroupHelpLink,
isPaidGroup: false,
confirmationPhrase,
@ -63,7 +63,7 @@ describe('Transfer group form', () => {
includeHeaders: false,
emptyNamespaceTitle: 'No parent group',
includeEmptyNamespace: true,
data: { groups },
groupNamespaces,
});
});
@ -91,7 +91,7 @@ describe('Transfer group form', () => {
});
describe('with a selected project', () => {
const [firstGroup] = groups;
const [firstGroup] = groupNamespaces;
beforeEach(() => {
wrapper = createComponent();
findNamespaceSelect().vm.$emit('select', firstGroup);

View file

@ -1,4 +1,7 @@
import { namespaces } from 'jest/vue_shared/components/namespace_select/mock_data';
import {
groupNamespaces,
userNamespaces,
} from 'jest/vue_shared/components/namespace_select/mock_data';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue';
import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
@ -13,7 +16,8 @@ describe('Transfer project form', () => {
const createComponent = () =>
shallowMountExtended(TransferProjectForm, {
propsData: {
namespaces,
userNamespaces,
groupNamespaces,
confirmButtonText,
confirmationPhrase,
},
@ -43,7 +47,7 @@ describe('Transfer project form', () => {
});
describe('with a selected namespace', () => {
const [selectedItem] = namespaces.group;
const [selectedItem] = groupNamespaces;
beforeEach(() => {
findNamespaceSelect().vm.$emit('select', selectedItem);

View file

@ -1,11 +1,6 @@
export const group = [
export const groupNamespaces = [
{ id: 1, name: 'Group 1', humanName: 'Group 1' },
{ id: 2, name: 'Subgroup 1', humanName: 'Group 1 / Subgroup 1' },
];
export const user = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }];
export const namespaces = {
group,
user,
};
export const userNamespaces = [{ id: 3, name: 'User namespace 1', humanName: 'User namespace 1' }];

View file

@ -5,9 +5,9 @@ import NamespaceSelect, {
i18n,
EMPTY_NAMESPACE_ID,
} from '~/vue_shared/components/namespace_select/namespace_select.vue';
import { user, group, namespaces } from './mock_data';
import { userNamespaces, groupNamespaces } from './mock_data';
const FLAT_NAMESPACES = [...group, ...user];
const FLAT_NAMESPACES = [...groupNamespaces, ...userNamespaces];
const EMPTY_NAMESPACE_TITLE = 'Empty namespace TEST';
const EMPTY_NAMESPACE_ITEM = { id: EMPTY_NAMESPACE_ID, humanName: EMPTY_NAMESPACE_TITLE };
@ -17,7 +17,8 @@ describe('Namespace Select', () => {
const createComponent = (props = {}) =>
shallowMountExtended(NamespaceSelect, {
propsData: {
data: namespaces,
userNamespaces,
groupNamespaces,
...props,
},
stubs: {
@ -89,11 +90,11 @@ describe('Namespace Select', () => {
describe('with search', () => {
it.each`
term | includeEmptyNamespace | expectedItems
${''} | ${false} | ${[...namespaces.group, ...namespaces.user]}
${'sub'} | ${false} | ${[namespaces.group[1]]}
${'User'} | ${false} | ${[...namespaces.user]}
${'User'} | ${true} | ${[...namespaces.user]}
${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...namespaces.user]}
${''} | ${false} | ${[...groupNamespaces, ...userNamespaces]}
${'sub'} | ${false} | ${[groupNamespaces[1]]}
${'User'} | ${false} | ${[...userNamespaces]}
${'User'} | ${true} | ${[...userNamespaces]}
${'namespace'} | ${true} | ${[EMPTY_NAMESPACE_ITEM, ...userNamespaces]}
`(
'with term=$term and includeEmptyNamespace=$includeEmptyNamespace, should show $expectedItems.length',
async ({ term, includeEmptyNamespace, expectedItems }) => {
@ -115,7 +116,7 @@ describe('Namespace Select', () => {
describe('with a selected namespace', () => {
const selectedGroupIndex = 1;
const selectedItem = group[selectedGroupIndex];
const selectedItem = groupNamespaces[selectedGroupIndex];
beforeEach(() => {
wrapper = createComponent();

View file

@ -5,23 +5,8 @@ require 'spec_helper'
RSpec.describe Sidebars::Concerns::WorkItemHierarchy do
shared_examples 'hierarchy menu' do
let(:item_id) { :hierarchy }
context 'when the feature is disabled does not render' do
before do
stub_feature_flags(work_items_hierarchy: false)
end
specify { is_expected.to be_nil }
end
context 'when the feature is enabled does render' do
before do
stub_feature_flags(work_items_hierarchy: true)
end
specify { is_expected.not_to be_nil }
end
end
describe 'Project hierarchy menu item' do
let_it_be_with_reload(:project) { create(:project, :repository) }

View file

@ -63,21 +63,7 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
describe 'Hierarchy' do
let(:item_id) { :hierarchy }
context 'when the feature is disabled' do
before do
stub_feature_flags(work_items_hierarchy: false)
end
specify { is_expected.to be_nil }
end
context 'when the feature is enabled' do
before do
stub_feature_flags(work_items_hierarchy: true)
end
specify { is_expected.not_to be_nil }
end
end
end
end

View file

@ -14,21 +14,10 @@ RSpec.describe PlanningHierarchy, type: :request do
describe 'GET #planning_hierarchy' do
it 'renders planning hierarchy' do
stub_feature_flags(work_items_hierarchy: true)
get project_planning_hierarchy_path(project)
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to match(/id="js-work-items-hierarchy"/)
end
it 'renders 404 page' do
stub_feature_flags(work_items_hierarchy: false)
get project_planning_hierarchy_path(project)
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).not_to match(/id="js-work-items-hierarchy"/)
end
end
end