Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-21 06:12:30 +00:00
parent b82d691107
commit b86ad5f488
18 changed files with 191 additions and 58 deletions

View file

@ -1579,7 +1579,7 @@
changes: *code-qa-patterns
when: manual
allow_failure: true
- <<: *if-dot-com-gitlab-org-schedule-child-pipeline
- <<: *if-dot-com-gitlab-org-schedule
allow_failure: true
.review:rules:review-stop:

View file

@ -13,7 +13,7 @@ export default {
ConfidentialityFilter,
},
computed: {
...mapState(['query']),
...mapState(['query', 'sidebarDirty']),
showReset() {
return this.query.state || this.query.confidential;
},
@ -32,7 +32,7 @@ export default {
<status-filter />
<confidentiality-filter />
<div class="gl-display-flex gl-align-items-center gl-mt-3">
<gl-button category="primary" variant="confirm" size="small" type="submit">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
</gl-button>
<gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{

View file

@ -5,7 +5,7 @@ const header = __('Status');
const filters = {
ANY: {
label: __('Any'),
value: 'all',
value: null,
},
OPEN: {
label: __('Open'),

View file

@ -2,9 +2,9 @@ import Api from '~/api';
import createFlash from '~/flash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, SIDEBAR_PARAMS } from './constants';
import * as types from './mutation_types';
import { loadDataFromLS, setFrequentItemToLS, mergeById } from './utils';
import { loadDataFromLS, setFrequentItemToLS, mergeById, isSidebarDirty } from './utils';
export const fetchGroups = ({ commit }, search) => {
commit(types.REQUEST_GROUPS);
@ -86,8 +86,12 @@ export const setFrequentProject = ({ state, commit }, item) => {
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: frequentItems });
};
export const setQuery = ({ commit }, { key, value }) => {
export const setQuery = ({ state, commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value });
if (SIDEBAR_PARAMS.includes(key)) {
commit(types.SET_SIDEBAR_DIRTY, isSidebarDirty(state.query, state.urlQuery));
}
};
export const applyQuery = ({ state }) => {

View file

@ -1,3 +1,6 @@
import { stateFilterData } from '~/search/sidebar/constants/state_filter_data';
import { confidentialFilterData } from '~/search/sidebar/constants/confidential_filter_data';
export const MAX_FREQUENT_ITEMS = 5;
export const MAX_FREQUENCY = 5;
@ -5,3 +8,5 @@ export const MAX_FREQUENCY = 5;
export const GROUPS_LOCAL_STORAGE_KEY = 'global-search-frequent-groups';
export const PROJECTS_LOCAL_STORAGE_KEY = 'global-search-frequent-projects';
export const SIDEBAR_PARAMS = [stateFilterData.filterParam, confidentialFilterData.filterParam];

View file

@ -7,5 +7,6 @@ export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
export const SET_QUERY = 'SET_QUERY';
export const SET_SIDEBAR_DIRTY = 'SET_SIDEBAR_DIRTY';
export const LOAD_FREQUENT_ITEMS = 'LOAD_FREQUENT_ITEMS';

View file

@ -26,6 +26,9 @@ export default {
[types.SET_QUERY](state, { key, value }) {
state.query[key] = value;
},
[types.SET_SIDEBAR_DIRTY](state, value) {
state.sidebarDirty = value;
},
[types.LOAD_FREQUENT_ITEMS](state, { key, data }) {
state.frequentItems[key] = data;
},

View file

@ -1,6 +1,8 @@
import { cloneDeep } from 'lodash';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
const createState = ({ query }) => ({
urlQuery: cloneDeep(query),
query,
groups: [],
fetchingGroups: false,
@ -10,5 +12,6 @@ const createState = ({ query }) => ({
[GROUPS_LOCAL_STORAGE_KEY]: [],
[PROJECTS_LOCAL_STORAGE_KEY]: [],
},
sidebarDirty: false,
});
export default createState;

View file

@ -1,5 +1,5 @@
import AccessorUtilities from '../../lib/utils/accessor';
import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY } from './constants';
import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY, SIDEBAR_PARAMS } from './constants';
function extractKeys(object, keyList) {
return Object.fromEntries(keyList.map((key) => [key, object[key]]));
@ -80,3 +80,13 @@ export const mergeById = (inflatedData, storedData) => {
return { ...stored, ...data };
});
};
export const isSidebarDirty = (currentQuery, urlQuery) => {
return SIDEBAR_PARAMS.some((param) => {
// userAddParam ensures we don't get a false dirty from null !== undefined
const userAddedParam = !urlQuery[param] && currentQuery[param];
const userChangedExistingParam = urlQuery[param] && urlQuery[param] !== currentQuery[param];
return userAddedParam || userChangedExistingParam;
});
};

View file

@ -341,9 +341,8 @@ Ensure your SAML identity provider sends an attribute statement named `Groups` o
```
NOTE:
The value for `Groups` or `groups` in the SAML response can be either the group name or the group ID.
To inspect the SAML response, you can use one of these [SAML debugging tools](#saml-debugging-tools).
Also note that the value for `Groups` or `groups` in the SAML response can be either the group name or
the group ID depending what the IdP sends to GitLab.
When SAML SSO is enabled for the top-level group, `Maintainer` and `Owner` level users
see a new menu item in group **Settings > SAML Group Links**. You can configure one or more **SAML Group Links** to map

View file

@ -78,10 +78,7 @@ the author's:
- `slug`
- `displayName`
If the user is not found by any of these properties, the search falls back to the author's
`email` address.
Alternatively, if there is also no email address, the project creator is set as the author.
If the user is not found by any of these properties, the project creator is set as the author.
##### Enable or disable User assignment by username

View file

@ -135,11 +135,11 @@ To learn more, see [Coverage check approval rule](../../../../ci/pipelines/setti
## Merge request approval settings cascading
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285410) in GitLab 14.4. [Deployed behind the `group_merge_request_approval_settings_feature_flag` flag](../../../../administration/feature_flags.md), disabled by default.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285410) in GitLab 14.4. [Deployed behind the `group_merge_request_approval_settings_feature_flag` flag](../../../../administration/feature_flags.md), disabled by default.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/285410) in GitLab 14.5.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the `group_merge_request_approval_settings_feature_flag` flag](../../../../administration/feature_flags.md). On GitLab.com, this feature is not available.
You should not use this feature for production environments
On self-managed GitLab, by default this feature is available. To hide the feature per group, ask an administrator to [disable the feature flag](../../../../administration/feature_flags.md) named `group_merge_request_approval_settings_feature_flag`. On GitLab.com, this feature is available.
You can also enforce merge request approval settings:

View file

@ -461,10 +461,14 @@ module Gitlab
end
def uid(rep_object)
find_user_id(by: :email, value: rep_object.author_email) unless Feature.enabled?(:bitbucket_server_user_mapping_by_username)
find_user_id(by: :username, value: rep_object.author_username) ||
# We want this explicit to only be username on the FF
# Otherwise, match email.
# There should be no default fall-through on username. Fall-through to import user
if Feature.enabled?(:bitbucket_server_user_mapping_by_username)
find_user_id(by: :username, value: rep_object.author_username)
else
find_user_id(by: :email, value: rep_object.author_email)
end
end
end
end

View file

@ -1,13 +1,13 @@
import { GlButton, GlLink } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
Vue.use(Vuex);
describe('GlobalSearchSidebar', () => {
let wrapper;
@ -27,21 +27,19 @@ describe('GlobalSearchSidebar', () => {
});
wrapper = shallowMount(GlobalSearchSidebar, {
localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findSidebarForm = () => wrapper.find('form');
const findStatusFilter = () => wrapper.find(StatusFilter);
const findConfidentialityFilter = () => wrapper.find(ConfidentialityFilter);
const findApplyButton = () => wrapper.find(GlButton);
const findResetLinkButton = () => wrapper.find(GlLink);
const findStatusFilter = () => wrapper.findComponent(StatusFilter);
const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
const findApplyButton = () => wrapper.findComponent(GlButton);
const findResetLinkButton = () => wrapper.findComponent(GlLink);
describe('template', () => {
beforeEach(() => {
@ -61,6 +59,28 @@ describe('GlobalSearchSidebar', () => {
});
});
describe('ApplyButton', () => {
describe('when sidebarDirty is false', () => {
beforeEach(() => {
createComponent({ sidebarDirty: false });
});
it('disables the button', () => {
expect(findApplyButton().attributes('disabled')).toBe('true');
});
});
describe('when sidebarDirty is true', () => {
beforeEach(() => {
createComponent({ sidebarDirty: true });
});
it('enables the button', () => {
expect(findApplyButton().attributes('disabled')).toBe(undefined);
});
});
});
describe('ResetLinkButton', () => {
describe('with no filter selected', () => {
beforeEach(() => {

View file

@ -5,7 +5,11 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import {
GROUPS_LOCAL_STORAGE_KEY,
PROJECTS_LOCAL_STORAGE_KEY,
SIDEBAR_PARAMS,
} from '~/search/store/constants';
import * as types from '~/search/store/mutation_types';
import createState from '~/search/store/state';
import * as storeUtils from '~/search/store/utils';
@ -153,15 +157,24 @@ describe('Global Search Store Actions', () => {
});
});
describe('setQuery', () => {
const payload = { key: 'key1', value: 'value1' };
describe.each`
payload | isDirty | isDirtyMutation
${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]}
${{ key: SIDEBAR_PARAMS[0], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]}
${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${false} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: false }]}
${{ key: SIDEBAR_PARAMS[1], value: 'test' }} | ${true} | ${[{ type: types.SET_SIDEBAR_DIRTY, payload: true }]}
${{ key: 'non-sidebar', value: 'test' }} | ${false} | ${[]}
${{ key: 'non-sidebar', value: 'test' }} | ${true} | ${[]}
`('setQuery', ({ payload, isDirty, isDirtyMutation }) => {
describe(`when filter param is ${payload.key} and utils.isSidebarDirty returns ${isDirty}`, () => {
const expectedMutations = [{ type: types.SET_QUERY, payload }].concat(isDirtyMutation);
it('calls the SET_QUERY mutation', () => {
return testAction({
action: actions.setQuery,
payload,
state,
expectedMutations: [{ type: types.SET_QUERY, payload }],
beforeEach(() => {
storeUtils.isSidebarDirty = jest.fn().mockReturnValue(isDirty);
});
it(`should dispatch the correct mutations`, () => {
return testAction({ action: actions.setQuery, payload, state, expectedMutations });
});
});
});

View file

@ -72,6 +72,16 @@ describe('Global Search Store Mutations', () => {
});
});
describe('SET_SIDEBAR_DIRTY', () => {
const value = true;
it('sets sidebarDirty to the value', () => {
mutations[types.SET_SIDEBAR_DIRTY](state, value);
expect(state.sidebarDirty).toBe(value);
});
});
describe('LOAD_FREQUENT_ITEMS', () => {
it('sets frequentItems[key] to data', () => {
const payload = { key: 'test-key', data: [1, 2, 3] };

View file

@ -1,6 +1,11 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { MAX_FREQUENCY } from '~/search/store/constants';
import { loadDataFromLS, setFrequentItemToLS, mergeById } from '~/search/store/utils';
import { MAX_FREQUENCY, SIDEBAR_PARAMS } from '~/search/store/constants';
import {
loadDataFromLS,
setFrequentItemToLS,
mergeById,
isSidebarDirty,
} from '~/search/store/utils';
import {
MOCK_LS_KEY,
MOCK_GROUPS,
@ -216,4 +221,24 @@ describe('Global Search Store Utils', () => {
});
});
});
describe.each`
description | currentQuery | urlQuery | isDirty
${'identical'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${false}
${'different'} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: 'default', [SIDEBAR_PARAMS[1]]: 'default' }} | ${true}
${'null/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: null, [SIDEBAR_PARAMS[1]]: null }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${false}
${'updated/undefined'} | ${{ [SIDEBAR_PARAMS[0]]: 'new', [SIDEBAR_PARAMS[1]]: 'new' }} | ${{ [SIDEBAR_PARAMS[0]]: undefined, [SIDEBAR_PARAMS[1]]: undefined }} | ${true}
`('isSidebarDirty', ({ description, currentQuery, urlQuery, isDirty }) => {
describe(`with ${description} sidebar query data`, () => {
let res;
beforeEach(() => {
res = isSidebarDirty(currentQuery, urlQuery);
});
it(`returns ${isDirty}`, () => {
expect(res).toStrictEqual(isDirty);
});
});
});
});

View file

@ -142,7 +142,7 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first
expect(merge_request.author).to eq(pull_request_author)
expect(merge_request.author).to eq(expected_author)
end
end
@ -151,7 +151,25 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
stub_feature_flags(bitbucket_server_user_mapping_by_username: false)
end
include_examples 'imports pull requests'
context 'when email is not present' do
before do
allow(pull_request).to receive(:author_email).and_return(nil)
end
let(:expected_author) { project_creator }
include_examples 'imports pull requests'
end
context 'when email is present' do
before do
allow(pull_request).to receive(:author_email).and_return(pull_request_author.email)
end
let(:expected_author) { pull_request_author }
include_examples 'imports pull requests'
end
end
context 'when bitbucket_server_user_mapping_by_username feature flag is enabled' do
@ -159,19 +177,24 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
stub_feature_flags(bitbucket_server_user_mapping_by_username: true)
end
include_examples 'imports pull requests' do
context 'when username is not present' do
before do
allow(pull_request).to receive(:author_username).and_return(nil)
end
it 'maps by email' do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first
expect(merge_request.author).to eq(pull_request_author)
end
context 'when username is not present' do
before do
allow(pull_request).to receive(:author_username).and_return(nil)
end
let(:expected_author) { project_creator }
include_examples 'imports pull requests'
end
context 'when username is present' do
before do
allow(pull_request).to receive(:author_username).and_return(pull_request_author.username)
end
let(:expected_author) { pull_request_author }
include_examples 'imports pull requests'
end
end
@ -228,7 +251,23 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
allow(subject.client).to receive(:activities).and_return([pr_comment])
end
it 'maps by email' do
it 'defaults to import user' do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first
expect(merge_request.notes.count).to eq(1)
note = merge_request.notes.first
expect(note.author).to eq(project_creator)
end
end
context 'when username is present' do
before do
allow(pr_note).to receive(:author_username).and_return(note_author.username)
allow(subject.client).to receive(:activities).and_return([pr_comment])
end
it 'maps by username' do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first
@ -384,13 +423,13 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
allow(inline_note).to receive(:author_username).and_return(nil)
end
it 'maps by email' do
it 'defaults to import user' do
expect { subject.execute }.to change { MergeRequest.count }.by(1)
notes = MergeRequest.first.notes.order(:id).to_a
expect(notes.first.author).to eq(inline_note_author)
expect(notes.last.author).to eq(reply_author)
expect(notes.first.author).to eq(project_creator)
expect(notes.last.author).to eq(project_creator)
end
end
end