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 changes: *code-qa-patterns
when: manual when: manual
allow_failure: true allow_failure: true
- <<: *if-dot-com-gitlab-org-schedule-child-pipeline - <<: *if-dot-com-gitlab-org-schedule
allow_failure: true allow_failure: true
.review:rules:review-stop: .review:rules:review-stop:

View file

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

View file

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

View file

@ -2,9 +2,9 @@ import Api from '~/api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility'; import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale'; 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 * as types from './mutation_types';
import { loadDataFromLS, setFrequentItemToLS, mergeById } from './utils'; import { loadDataFromLS, setFrequentItemToLS, mergeById, isSidebarDirty } from './utils';
export const fetchGroups = ({ commit }, search) => { export const fetchGroups = ({ commit }, search) => {
commit(types.REQUEST_GROUPS); 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 }); 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 }); 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 }) => { 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_FREQUENT_ITEMS = 5;
export const MAX_FREQUENCY = 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 GROUPS_LOCAL_STORAGE_KEY = 'global-search-frequent-groups';
export const PROJECTS_LOCAL_STORAGE_KEY = 'global-search-frequent-projects'; 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 RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
export const SET_QUERY = 'SET_QUERY'; export const SET_QUERY = 'SET_QUERY';
export const SET_SIDEBAR_DIRTY = 'SET_SIDEBAR_DIRTY';
export const LOAD_FREQUENT_ITEMS = 'LOAD_FREQUENT_ITEMS'; export const LOAD_FREQUENT_ITEMS = 'LOAD_FREQUENT_ITEMS';

View file

@ -26,6 +26,9 @@ export default {
[types.SET_QUERY](state, { key, value }) { [types.SET_QUERY](state, { key, value }) {
state.query[key] = value; state.query[key] = value;
}, },
[types.SET_SIDEBAR_DIRTY](state, value) {
state.sidebarDirty = value;
},
[types.LOAD_FREQUENT_ITEMS](state, { key, data }) { [types.LOAD_FREQUENT_ITEMS](state, { key, data }) {
state.frequentItems[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'; import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
const createState = ({ query }) => ({ const createState = ({ query }) => ({
urlQuery: cloneDeep(query),
query, query,
groups: [], groups: [],
fetchingGroups: false, fetchingGroups: false,
@ -10,5 +12,6 @@ const createState = ({ query }) => ({
[GROUPS_LOCAL_STORAGE_KEY]: [], [GROUPS_LOCAL_STORAGE_KEY]: [],
[PROJECTS_LOCAL_STORAGE_KEY]: [], [PROJECTS_LOCAL_STORAGE_KEY]: [],
}, },
sidebarDirty: false,
}); });
export default createState; export default createState;

View file

@ -1,5 +1,5 @@
import AccessorUtilities from '../../lib/utils/accessor'; 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) { function extractKeys(object, keyList) {
return Object.fromEntries(keyList.map((key) => [key, object[key]])); return Object.fromEntries(keyList.map((key) => [key, object[key]]));
@ -80,3 +80,13 @@ export const mergeById = (inflatedData, storedData) => {
return { ...stored, ...data }; 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: 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). 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 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 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` - `slug`
- `displayName` - `displayName`
If the user is not found by any of these properties, the search falls back to the author's If the user is not found by any of these properties, the project creator is set as the author.
`email` address.
Alternatively, if there is also no email address, the project creator is set as the author.
##### Enable or disable User assignment by username ##### 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 ## 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: 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. 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 should not use this feature for production environments
You can also enforce merge request approval settings: You can also enforce merge request approval settings:

View file

@ -461,11 +461,15 @@ module Gitlab
end end
def uid(rep_object) def uid(rep_object)
find_user_id(by: :email, value: rep_object.author_email) unless Feature.enabled?(:bitbucket_server_user_mapping_by_username) # We want this explicit to only be username on the FF
# Otherwise, match email.
find_user_id(by: :username, value: rep_object.author_username) || # 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) find_user_id(by: :email, value: rep_object.author_email)
end end
end end
end end
end end
end

View file

@ -1,13 +1,13 @@
import { GlButton, GlLink } from '@gitlab/ui'; 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 Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data'; import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue'; import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue'; import ConfidentialityFilter from '~/search/sidebar/components/confidentiality_filter.vue';
import StatusFilter from '~/search/sidebar/components/status_filter.vue'; import StatusFilter from '~/search/sidebar/components/status_filter.vue';
const localVue = createLocalVue(); Vue.use(Vuex);
localVue.use(Vuex);
describe('GlobalSearchSidebar', () => { describe('GlobalSearchSidebar', () => {
let wrapper; let wrapper;
@ -27,21 +27,19 @@ describe('GlobalSearchSidebar', () => {
}); });
wrapper = shallowMount(GlobalSearchSidebar, { wrapper = shallowMount(GlobalSearchSidebar, {
localVue,
store, store,
}); });
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
const findSidebarForm = () => wrapper.find('form'); const findSidebarForm = () => wrapper.find('form');
const findStatusFilter = () => wrapper.find(StatusFilter); const findStatusFilter = () => wrapper.findComponent(StatusFilter);
const findConfidentialityFilter = () => wrapper.find(ConfidentialityFilter); const findConfidentialityFilter = () => wrapper.findComponent(ConfidentialityFilter);
const findApplyButton = () => wrapper.find(GlButton); const findApplyButton = () => wrapper.findComponent(GlButton);
const findResetLinkButton = () => wrapper.find(GlLink); const findResetLinkButton = () => wrapper.findComponent(GlLink);
describe('template', () => { describe('template', () => {
beforeEach(() => { 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('ResetLinkButton', () => {
describe('with no filter selected', () => { describe('with no filter selected', () => {
beforeEach(() => { beforeEach(() => {

View file

@ -5,7 +5,11 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions'; 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 * as types from '~/search/store/mutation_types';
import createState from '~/search/store/state'; import createState from '~/search/store/state';
import * as storeUtils from '~/search/store/utils'; import * as storeUtils from '~/search/store/utils';
@ -153,15 +157,24 @@ describe('Global Search Store Actions', () => {
}); });
}); });
describe('setQuery', () => { describe.each`
const payload = { key: 'key1', value: 'value1' }; 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', () => { beforeEach(() => {
return testAction({ storeUtils.isSidebarDirty = jest.fn().mockReturnValue(isDirty);
action: actions.setQuery, });
payload,
state, it(`should dispatch the correct mutations`, () => {
expectedMutations: [{ type: types.SET_QUERY, payload }], 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', () => { describe('LOAD_FREQUENT_ITEMS', () => {
it('sets frequentItems[key] to data', () => { it('sets frequentItems[key] to data', () => {
const payload = { key: 'test-key', data: [1, 2, 3] }; const payload = { key: 'test-key', data: [1, 2, 3] };

View file

@ -1,6 +1,11 @@
import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { MAX_FREQUENCY } from '~/search/store/constants'; import { MAX_FREQUENCY, SIDEBAR_PARAMS } from '~/search/store/constants';
import { loadDataFromLS, setFrequentItemToLS, mergeById } from '~/search/store/utils'; import {
loadDataFromLS,
setFrequentItemToLS,
mergeById,
isSidebarDirty,
} from '~/search/store/utils';
import { import {
MOCK_LS_KEY, MOCK_LS_KEY,
MOCK_GROUPS, 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) expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first merge_request = MergeRequest.first
expect(merge_request.author).to eq(pull_request_author) expect(merge_request.author).to eq(expected_author)
end end
end end
@ -151,27 +151,50 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
stub_feature_flags(bitbucket_server_user_mapping_by_username: false) stub_feature_flags(bitbucket_server_user_mapping_by_username: false)
end end
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' include_examples 'imports pull requests'
end 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 context 'when bitbucket_server_user_mapping_by_username feature flag is enabled' do
before do before do
stub_feature_flags(bitbucket_server_user_mapping_by_username: true) stub_feature_flags(bitbucket_server_user_mapping_by_username: true)
end end
include_examples 'imports pull requests' do
context 'when username is not present' do context 'when username is not present' do
before do before do
allow(pull_request).to receive(:author_username).and_return(nil) allow(pull_request).to receive(:author_username).and_return(nil)
end end
it 'maps by email' do let(:expected_author) { project_creator }
expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first include_examples 'imports pull requests'
expect(merge_request.author).to eq(pull_request_author)
end end
context 'when username is present' do
before do
allow(pull_request).to receive(:author_username).and_return(pull_request_author.username)
end end
let(:expected_author) { pull_request_author }
include_examples 'imports pull requests'
end end
end end
@ -228,7 +251,23 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
allow(subject.client).to receive(:activities).and_return([pr_comment]) allow(subject.client).to receive(:activities).and_return([pr_comment])
end 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) expect { subject.execute }.to change { MergeRequest.count }.by(1)
merge_request = MergeRequest.first merge_request = MergeRequest.first
@ -384,13 +423,13 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
allow(inline_note).to receive(:author_username).and_return(nil) allow(inline_note).to receive(:author_username).and_return(nil)
end end
it 'maps by email' do it 'defaults to import user' do
expect { subject.execute }.to change { MergeRequest.count }.by(1) expect { subject.execute }.to change { MergeRequest.count }.by(1)
notes = MergeRequest.first.notes.order(:id).to_a notes = MergeRequest.first.notes.order(:id).to_a
expect(notes.first.author).to eq(inline_note_author) expect(notes.first.author).to eq(project_creator)
expect(notes.last.author).to eq(reply_author) expect(notes.last.author).to eq(project_creator)
end end
end end
end end