Commit 4733817a authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch '21268-cherry-pick-accross-forks-fe' into 'master'

Add the ability to cherry pick accross forks [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!55970
parents df770ee2 107b28be
...@@ -7,7 +7,11 @@ import { ...@@ -7,7 +7,11 @@ import {
GlLoadingIcon, GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { I18N_DROPDOWN } from '../constants'; import {
I18N_NO_RESULTS_MESSAGE,
I18N_BRANCH_HEADER,
I18N_BRANCH_SEARCH_PLACEHOLDER,
} from '../constants';
export default { export default {
name: 'BranchesDropdown', name: 'BranchesDropdown',
...@@ -25,7 +29,11 @@ export default { ...@@ -25,7 +29,11 @@ export default {
default: '', default: '',
}, },
}, },
i18n: I18N_DROPDOWN, i18n: {
noResultsMessage: I18N_NO_RESULTS_MESSAGE,
branchHeaderTitle: I18N_BRANCH_HEADER,
branchSearchPlaceholder: I18N_BRANCH_SEARCH_PLACEHOLDER,
},
data() { data() {
return { return {
searchTerm: this.value, searchTerm: this.value,
...@@ -41,6 +49,13 @@ export default { ...@@ -41,6 +49,13 @@ export default {
); );
}, },
}, },
watch: {
// Parent component can set the branch value (e.g. when the user selects a different project)
// and we need to keep the search term in sync with the selected value
value(val) {
this.searchTermChanged(val);
},
},
mounted() { mounted() {
this.fetchBranches(this.searchTerm); this.fetchBranches(this.searchTerm);
}, },
...@@ -61,13 +76,13 @@ export default { ...@@ -61,13 +76,13 @@ export default {
}; };
</script> </script>
<template> <template>
<gl-dropdown :text="value" :header-text="$options.i18n.headerTitle"> <gl-dropdown :text="value" :header-text="$options.i18n.branchHeaderTitle">
<gl-search-box-by-type <gl-search-box-by-type
:value="searchTerm" :value="searchTerm"
trim trim
autocomplete="off" autocomplete="off"
:debounce="250" :debounce="250"
:placeholder="$options.i18n.searchPlaceholder" :placeholder="$options.i18n.branchSearchPlaceholder"
data-testid="dropdown-search-box" data-testid="dropdown-search-box"
@input="searchTermChanged" @input="searchTermChanged"
/> />
......
...@@ -3,18 +3,22 @@ import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab ...@@ -3,18 +3,22 @@ import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import BranchesDropdown from './branches_dropdown.vue'; import BranchesDropdown from './branches_dropdown.vue';
import ProjectsDropdown from './projects_dropdown.vue';
export default { export default {
components: { components: {
BranchesDropdown, BranchesDropdown,
ProjectsDropdown,
GlModal, GlModal,
GlForm, GlForm,
GlFormCheckbox, GlFormCheckbox,
GlSprintf, GlSprintf,
GlFormGroup, GlFormGroup,
}, },
mixins: [glFeatureFlagsMixin()],
inject: { inject: {
prependedText: { prependedText: {
default: '', default: '',
...@@ -60,13 +64,17 @@ export default { ...@@ -60,13 +64,17 @@ export default {
'modalTitle', 'modalTitle',
'existingBranch', 'existingBranch',
'prependedText', 'prependedText',
'targetProjectId',
'targetProjectName',
'branchesEndpoint',
]), ]),
}, },
mounted() { mounted() {
this.setSelectedProject(this.targetProjectId);
eventHub.$on(this.openModal, this.show); eventHub.$on(this.openModal, this.show);
}, },
methods: { methods: {
...mapActions(['clearModal', 'setBranch', 'setSelectedBranch']), ...mapActions(['clearModal', 'setBranch', 'setSelectedBranch', 'setSelectedProject']),
show() { show() {
this.$root.$emit(BV_SHOW_MODAL, this.modalId); this.$root.$emit(BV_SHOW_MODAL, this.modalId);
}, },
...@@ -101,6 +109,26 @@ export default { ...@@ -101,6 +109,26 @@ export default {
<gl-form ref="form" :action="endpoint" method="post"> <gl-form ref="form" :action="endpoint" method="post">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" /> <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<gl-form-group
v-if="glFeatures.pickIntoProject"
:label="i18n.projectLabel"
label-for="start_project"
data-testid="dropdown-group"
>
<input
id="target_project_id"
type="hidden"
name="target_project_id"
:value="targetProjectId"
/>
<projects-dropdown
class="gl-w-half"
:value="targetProjectName"
@selectProject="setSelectedProject"
/>
</gl-form-group>
<gl-form-group <gl-form-group
:label="i18n.branchLabel" :label="i18n.branchLabel"
label-for="start_branch" label-for="start_branch"
......
<script>
import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlDropdownText } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import {
I18N_NO_RESULTS_MESSAGE,
I18N_PROJECT_HEADER,
I18N_PROJECT_SEARCH_PLACEHOLDER,
} from '../constants';
export default {
name: 'ProjectsDropdown',
components: {
GlDropdown,
GlSearchBoxByType,
GlDropdownItem,
GlDropdownText,
},
props: {
value: {
type: String,
required: false,
default: '',
},
},
i18n: {
noResultsMessage: I18N_NO_RESULTS_MESSAGE,
projectHeaderTitle: I18N_PROJECT_HEADER,
projectSearchPlaceholder: I18N_PROJECT_SEARCH_PLACEHOLDER,
},
data() {
return {
filterTerm: this.value,
};
},
computed: {
...mapGetters(['sortedProjects']),
...mapState(['targetProjectId']),
filteredResults() {
const lowerCasedFilterTerm = this.filterTerm.toLowerCase();
return this.sortedProjects.filter((project) =>
project.name.toLowerCase().includes(lowerCasedFilterTerm),
);
},
selectedProject() {
return this.sortedProjects.find((project) => project.id === this.targetProjectId) || {};
},
},
methods: {
selectProject(project) {
this.$emit('selectProject', project.id);
this.filterTerm = project.name; // when we select a project, we want the dropdown to filter to the selected project
},
isSelected(selectedProject) {
return selectedProject === this.selectedProject;
},
filterTermChanged(value) {
this.filterTerm = value;
},
},
};
</script>
<template>
<gl-dropdown :text="selectedProject.name" :header-text="$options.i18n.projectHeaderTitle">
<gl-search-box-by-type
:value="filterTerm"
trim
autocomplete="off"
:placeholder="$options.i18n.projectSearchPlaceholder"
data-testid="dropdown-search-box"
@input="filterTermChanged"
/>
<gl-dropdown-item
v-for="project in filteredResults"
:key="project.name"
:name="project.name"
:is-checked="isSelected(project)"
is-check-item
data-testid="dropdown-item"
@click="selectProject(project)"
>
{{ project.name }}
</gl-dropdown-item>
<gl-dropdown-text v-if="!filteredResults.length" data-testid="empty-result-message">
<span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span>
</gl-dropdown-text>
</gl-dropdown>
</template>
...@@ -26,6 +26,7 @@ export const I18N_REVERT_MODAL = { ...@@ -26,6 +26,7 @@ export const I18N_REVERT_MODAL = {
export const I18N_CHERRY_PICK_MODAL = { export const I18N_CHERRY_PICK_MODAL = {
branchLabel: s__('ChangeTypeAction|Pick into branch'), branchLabel: s__('ChangeTypeAction|Pick into branch'),
projectLabel: s__('ChangeTypeAction|Pick into project'),
actionPrimaryText: s__('ChangeTypeAction|Cherry-pick'), actionPrimaryText: s__('ChangeTypeAction|Cherry-pick'),
}; };
...@@ -33,10 +34,12 @@ export const PREPENDED_MODAL_TEXT = s__( ...@@ -33,10 +34,12 @@ export const PREPENDED_MODAL_TEXT = s__(
'ChangeTypeAction|This will create a new commit in order to revert the existing changes.', 'ChangeTypeAction|This will create a new commit in order to revert the existing changes.',
); );
export const I18N_DROPDOWN = { export const I18N_NO_RESULTS_MESSAGE = __('No matching results');
noResultsMessage: __('No matching results'),
headerTitle: s__('ChangeTypeAction|Switch branch'), export const I18N_PROJECT_HEADER = s__('ChangeTypeAction|Switch project');
searchPlaceholder: s__('ChangeTypeAction|Search branches'), export const I18N_PROJECT_SEARCH_PLACEHOLDER = s__('ChangeTypeAction|Search projects');
};
export const I18N_BRANCH_HEADER = s__('ChangeTypeAction|Switch branch');
export const I18N_BRANCH_SEARCH_PLACEHOLDER = s__('ChangeTypeAction|Search branches');
export const PROJECT_BRANCHES_ERROR = __('Something went wrong while fetching branches'); export const PROJECT_BRANCHES_ERROR = __('Something went wrong while fetching branches');
import Vue from 'vue'; import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import CommitFormModal from './components/form_modal.vue'; import CommitFormModal from './components/form_modal.vue';
import { import {
I18N_MODAL, I18N_MODAL,
...@@ -19,21 +19,27 @@ export default function initInviteMembersModal() { ...@@ -19,21 +19,27 @@ export default function initInviteMembersModal() {
title, title,
endpoint, endpoint,
branch, branch,
targetProjectId,
targetProjectName,
pushCode, pushCode,
branchCollaboration, branchCollaboration,
existingBranch, existingBranch,
branchesEndpoint, branchesEndpoint,
projects,
} = el.dataset; } = el.dataset;
const store = createStore({ const store = createStore({
endpoint, endpoint,
branchesEndpoint, branchesEndpoint,
branch, branch,
targetProjectId,
targetProjectName,
pushCode: parseBoolean(pushCode), pushCode: parseBoolean(pushCode),
branchCollaboration: parseBoolean(branchCollaboration), branchCollaboration: parseBoolean(branchCollaboration),
defaultBranch: branch, defaultBranch: branch,
modalTitle: title, modalTitle: title,
existingBranch, existingBranch,
projects: convertObjectPropsToCamelCase(JSON.parse(projects), { deep: true }),
}); });
return new Vue({ return new Vue({
......
...@@ -11,6 +11,10 @@ export const requestBranches = ({ commit }) => { ...@@ -11,6 +11,10 @@ export const requestBranches = ({ commit }) => {
commit(types.REQUEST_BRANCHES); commit(types.REQUEST_BRANCHES);
}; };
export const setBranchesEndpoint = ({ commit }, endpoint) => {
commit(types.SET_BRANCHES_ENDPOINT, endpoint);
};
export const fetchBranches = ({ commit, dispatch, state }, query) => { export const fetchBranches = ({ commit, dispatch, state }, query) => {
dispatch('requestBranches'); dispatch('requestBranches');
...@@ -18,8 +22,8 @@ export const fetchBranches = ({ commit, dispatch, state }, query) => { ...@@ -18,8 +22,8 @@ export const fetchBranches = ({ commit, dispatch, state }, query) => {
.get(state.branchesEndpoint, { .get(state.branchesEndpoint, {
params: { search: query }, params: { search: query },
}) })
.then((res) => { .then(({ data }) => {
commit(types.RECEIVE_BRANCHES_SUCCESS, res.data); commit(types.RECEIVE_BRANCHES_SUCCESS, data.Branches || []);
}) })
.catch(() => { .catch(() => {
createFlash({ message: PROJECT_BRANCHES_ERROR }); createFlash({ message: PROJECT_BRANCHES_ERROR });
...@@ -34,3 +38,15 @@ export const setBranch = ({ commit, dispatch }, branch) => { ...@@ -34,3 +38,15 @@ export const setBranch = ({ commit, dispatch }, branch) => {
export const setSelectedBranch = ({ commit }, branch) => { export const setSelectedBranch = ({ commit }, branch) => {
commit(types.SET_SELECTED_BRANCH, branch); commit(types.SET_SELECTED_BRANCH, branch);
}; };
export const setSelectedProject = ({ commit, dispatch, state }, id) => {
let { branchesEndpoint } = state;
if (state.projects?.length) {
branchesEndpoint = state.projects.find((p) => p.id === id).refsUrl;
}
commit(types.SET_SELECTED_PROJECT, id);
dispatch('setBranchesEndpoint', branchesEndpoint);
dispatch('fetchBranches');
};
...@@ -3,3 +3,5 @@ import { uniq } from 'lodash'; ...@@ -3,3 +3,5 @@ import { uniq } from 'lodash';
export const joinedBranches = (state) => { export const joinedBranches = (state) => {
return uniq(state.branches).sort(); return uniq(state.branches).sort();
}; };
export const sortedProjects = (state) => uniq(state.projects).sort();
export const CLEAR_MODAL = 'CLEAR_MODAL'; export const CLEAR_MODAL = 'CLEAR_MODAL';
export const SET_BRANCHES_ENDPOINT = 'SET_BRANCHES_ENDPOINT';
export const REQUEST_BRANCHES = 'REQUEST_BRANCHES'; export const REQUEST_BRANCHES = 'REQUEST_BRANCHES';
export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS'; export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS';
export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH = 'SET_BRANCH';
export const SET_SELECTED_BRANCH = 'SET_SELECTED_BRANCH'; export const SET_SELECTED_BRANCH = 'SET_SELECTED_BRANCH';
export const SET_SELECTED_PROJECT = 'SET_SELECTED_PROJECT';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_BRANCHES_ENDPOINT](state, endpoint) {
state.branchesEndpoint = endpoint;
},
[types.REQUEST_BRANCHES](state) { [types.REQUEST_BRANCHES](state) {
state.isFetching = true; state.isFetching = true;
}, },
...@@ -22,4 +26,9 @@ export default { ...@@ -22,4 +26,9 @@ export default {
[types.SET_SELECTED_BRANCH](state, branch) { [types.SET_SELECTED_BRANCH](state, branch) {
state.selectedBranch = branch; state.selectedBranch = branch;
}, },
[types.SET_SELECTED_PROJECT](state, projectId) {
state.targetProjectId = projectId;
state.branch = state.defaultBranch;
},
}; };
...@@ -3,6 +3,7 @@ export default () => ({ ...@@ -3,6 +3,7 @@ export default () => ({
branchesEndpoint: null, branchesEndpoint: null,
isFetching: false, isFetching: false,
branches: [], branches: [],
projects: [],
selectedBranch: '', selectedBranch: '',
pushCode: false, pushCode: false,
branchCollaboration: false, branchCollaboration: false,
...@@ -10,4 +11,6 @@ export default () => ({ ...@@ -10,4 +11,6 @@ export default () => ({
existingBranch: '', existingBranch: '',
defaultBranch: '', defaultBranch: '',
branch: '', branch: '',
targetProjectId: '',
targetProjectName: '',
}); });
...@@ -24,6 +24,10 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -24,6 +24,10 @@ class Projects::CommitController < Projects::ApplicationController
push_frontend_feature_flag(:ci_commit_pipeline_mini_graph_vue, @project, default_enabled: :yaml) push_frontend_feature_flag(:ci_commit_pipeline_mini_graph_vue, @project, default_enabled: :yaml)
end end
before_action do
push_frontend_feature_flag(:pick_into_project)
end
BRANCH_SEARCH_LIMIT = 1000 BRANCH_SEARCH_LIMIT = 1000
COMMIT_DIFFS_PER_PAGE = 75 COMMIT_DIFFS_PER_PAGE = 75
......
...@@ -134,6 +134,16 @@ module CommitsHelper ...@@ -134,6 +134,16 @@ module CommitsHelper
end end
end end
def cherry_pick_projects_data(project)
target_projects(project).map do |project|
{
id: project.id.to_s,
name: project.full_path,
refsUrl: refs_project_path(project)
}
end
end
protected protected
# Private: Returns a link to a person. If the person has a matching user and # Private: Returns a link to a person. If the person has a matching user and
......
...@@ -18,7 +18,10 @@ ...@@ -18,7 +18,10 @@
.js-cherry-pick-commit-modal{ data: { title: title, .js-cherry-pick-commit-modal{ data: { title: title,
endpoint: cherry_pick_namespace_project_commit_path(commit, namespace_id: @project.namespace.full_path, project_id: @project), endpoint: cherry_pick_namespace_project_commit_path(commit, namespace_id: @project.namespace.full_path, project_id: @project),
branch: @project.default_branch, branch: @project.default_branch,
target_project_id: @project.id,
target_project_name: @project.full_path,
push_code: can?(current_user, :push_code, @project).to_s, push_code: can?(current_user, :push_code, @project).to_s,
branch_collaboration: @project.branch_allows_collaboration?(current_user, selected_branch).to_s, branch_collaboration: @project.branch_allows_collaboration?(current_user, selected_branch).to_s,
existing_branch: ERB::Util.html_escape(selected_branch), existing_branch: ERB::Util.html_escape(selected_branch),
branches_endpoint: project_branches_path(@project) } } branches_endpoint: refs_project_path(@project),
projects: cherry_pick_projects_data(@project).to_json } }
---
title: Add the ability to cherry pick accross forks
merge_request: 55970
author:
type: added
---
name: pick_into_project
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55970
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/324154
milestone: '13.10'
type: development
group: group::source code
default_enabled: false
...@@ -5642,6 +5642,9 @@ msgstr "" ...@@ -5642,6 +5642,9 @@ msgstr ""
msgid "ChangeTypeAction|Pick into branch" msgid "ChangeTypeAction|Pick into branch"
msgstr "" msgstr ""
msgid "ChangeTypeAction|Pick into project"
msgstr ""
msgid "ChangeTypeAction|Revert" msgid "ChangeTypeAction|Revert"
msgstr "" msgstr ""
...@@ -5651,12 +5654,18 @@ msgstr "" ...@@ -5651,12 +5654,18 @@ msgstr ""
msgid "ChangeTypeAction|Search branches" msgid "ChangeTypeAction|Search branches"
msgstr "" msgstr ""
msgid "ChangeTypeAction|Search projects"
msgstr ""
msgid "ChangeTypeAction|Start a %{newMergeRequest} with these changes" msgid "ChangeTypeAction|Start a %{newMergeRequest} with these changes"
msgstr "" msgstr ""
msgid "ChangeTypeAction|Switch branch" msgid "ChangeTypeAction|Switch branch"
msgstr "" msgstr ""
msgid "ChangeTypeAction|Switch project"
msgstr ""
msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes." msgid "ChangeTypeAction|This will create a new commit in order to revert the existing changes."
msgstr "" msgstr ""
......
...@@ -7,6 +7,7 @@ import axios from '~/lib/utils/axios_utils'; ...@@ -7,6 +7,7 @@ import axios from '~/lib/utils/axios_utils';
import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue'; import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
import CommitFormModal from '~/projects/commit/components/form_modal.vue'; import CommitFormModal from '~/projects/commit/components/form_modal.vue';
import ProjectsDropdown from '~/projects/commit/components/projects_dropdown.vue';
import eventHub from '~/projects/commit/event_hub'; import eventHub from '~/projects/commit/event_hub';
import createStore from '~/projects/commit/store'; import createStore from '~/projects/commit/store';
import mockData from '../mock_data'; import mockData from '../mock_data';
...@@ -20,7 +21,10 @@ describe('CommitFormModal', () => { ...@@ -20,7 +21,10 @@ describe('CommitFormModal', () => {
store = createStore({ ...mockData.mockModal, ...state }); store = createStore({ ...mockData.mockModal, ...state });
wrapper = extendedWrapper( wrapper = extendedWrapper(
method(CommitFormModal, { method(CommitFormModal, {
provide, provide: {
...provide,
glFeatures: { pickIntoProject: true },
},
propsData: { ...mockData.modalPropsData }, propsData: { ...mockData.modalPropsData },
store, store,
attrs: { attrs: {
...@@ -33,7 +37,9 @@ describe('CommitFormModal', () => { ...@@ -33,7 +37,9 @@ describe('CommitFormModal', () => {
const findModal = () => wrapper.findComponent(GlModal); const findModal = () => wrapper.findComponent(GlModal);
const findStartBranch = () => wrapper.find('#start_branch'); const findStartBranch = () => wrapper.find('#start_branch');
const findDropdown = () => wrapper.findComponent(BranchesDropdown); const findTargetProject = () => wrapper.find('#target_project_id');
const findBranchesDropdown = () => wrapper.findComponent(BranchesDropdown);
const findProjectsDropdown = () => wrapper.findComponent(ProjectsDropdown);
const findForm = () => findModal().findComponent(GlForm); const findForm = () => findModal().findComponent(GlForm);
const findCheckBox = () => findForm().findComponent(GlFormCheckbox); const findCheckBox = () => findForm().findComponent(GlFormCheckbox);
const findPrependedText = () => wrapper.findByTestId('prepended-text'); const findPrependedText = () => wrapper.findByTestId('prepended-text');
...@@ -146,11 +152,19 @@ describe('CommitFormModal', () => { ...@@ -146,11 +152,19 @@ describe('CommitFormModal', () => {
}); });
it('Changes the start_branch input value', async () => { it('Changes the start_branch input value', async () => {
findDropdown().vm.$emit('selectBranch', '_changed_branch_value_'); findBranchesDropdown().vm.$emit('selectBranch', '_changed_branch_value_');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findStartBranch().attributes('value')).toBe('_changed_branch_value_'); expect(findStartBranch().attributes('value')).toBe('_changed_branch_value_');
}); });
it('Changes the target_project_id input value', async () => {
findProjectsDropdown().vm.$emit('selectProject', '_changed_project_value_');
await wrapper.vm.$nextTick();
expect(findTargetProject().attributes('value')).toBe('_changed_project_value_');
});
}); });
}); });
import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ProjectsDropdown from '~/projects/commit/components/projects_dropdown.vue';
Vue.use(Vuex);
describe('ProjectsDropdown', () => {
let wrapper;
let store;
const spyFetchProjects = jest.fn();
const projectsMockData = [
{ id: '1', name: '_project_1_', refsUrl: '_project_1_/refs' },
{ id: '2', name: '_project_2_', refsUrl: '_project_2_/refs' },
{ id: '3', name: '_project_3_', refsUrl: '_project_3_/refs' },
];
const createComponent = (term, state = {}) => {
store = new Vuex.Store({
getters: {
sortedProjects: () => projectsMockData,
},
state,
});
wrapper = extendedWrapper(
shallowMount(ProjectsDropdown, {
store,
propsData: {
value: term,
},
}),
);
};
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
const findNoResults = () => wrapper.findByTestId('empty-result-message');
afterEach(() => {
wrapper.destroy();
spyFetchProjects.mockReset();
});
describe('No projects found', () => {
beforeEach(() => {
createComponent('_non_existent_project_');
});
it('renders empty results message', () => {
expect(findNoResults().text()).toBe('No matching results');
});
it('shows GlSearchBoxByType with default attributes', () => {
expect(findSearchBoxByType().exists()).toBe(true);
expect(findSearchBoxByType().vm.$attrs).toMatchObject({
placeholder: 'Search projects',
});
});
});
describe('Search term is empty', () => {
beforeEach(() => {
createComponent('');
});
it('renders all projects when search term is empty', () => {
expect(findAllDropdownItems()).toHaveLength(3);
expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
expect(findDropdownItemByIndex(1).text()).toBe('_project_2_');
expect(findDropdownItemByIndex(2).text()).toBe('_project_3_');
});
it('should not be selected on the inactive project', () => {
expect(wrapper.vm.isSelected('_project_1_')).toBe(false);
});
});
describe('Projects found', () => {
beforeEach(() => {
createComponent('_project_1_', { targetProjectId: '1' });
});
it('renders only the project searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
});
it('should not display empty results message', () => {
expect(findNoResults().exists()).toBe(false);
});
it('should signify this project is selected', () => {
expect(findDropdownItemByIndex(0).props('isChecked')).toBe(true);
});
it('should signify the project is not selected', () => {
expect(wrapper.vm.isSelected('_not_selected_project_')).toBe(false);
});
describe('Custom events', () => {
it('should emit selectProject if a project is clicked', () => {
findDropdownItemByIndex(0).vm.$emit('click');
expect(wrapper.emitted('selectProject')).toEqual([['1']]);
expect(wrapper.vm.filterTerm).toBe('_project_1_');
});
});
});
describe('Case insensitive for search term', () => {
beforeEach(() => {
createComponent('_PrOjEcT_1_');
});
it('renders only the project searched for', () => {
expect(findAllDropdownItems()).toHaveLength(1);
expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
});
});
});
...@@ -24,4 +24,5 @@ export default { ...@@ -24,4 +24,5 @@ export default {
openModal: '_open_modal_', openModal: '_open_modal_',
}, },
mockBranches: ['_branch_1', '_abc_', '_master_'], mockBranches: ['_branch_1', '_abc_', '_master_'],
mockProjects: ['_project_1', '_abc_', '_project_'],
}; };
...@@ -47,7 +47,7 @@ describe('Commit form modal store actions', () => { ...@@ -47,7 +47,7 @@ describe('Commit form modal store actions', () => {
it('dispatch correct actions on fetchBranches', (done) => { it('dispatch correct actions on fetchBranches', (done) => {
jest jest
.spyOn(axios, 'get') .spyOn(axios, 'get')
.mockImplementation(() => Promise.resolve({ data: mockData.mockBranches })); .mockImplementation(() => Promise.resolve({ data: { Branches: mockData.mockBranches } }));
testAction( testAction(
actions.fetchBranches, actions.fetchBranches,
...@@ -108,4 +108,43 @@ describe('Commit form modal store actions', () => { ...@@ -108,4 +108,43 @@ describe('Commit form modal store actions', () => {
]); ]);
}); });
}); });
describe('setBranchesEndpoint', () => {
it('commits SET_BRANCHES_ENDPOINT mutation', () => {
const endpoint = 'some/endpoint';
testAction(actions.setBranchesEndpoint, endpoint, {}, [
{
type: types.SET_BRANCHES_ENDPOINT,
payload: endpoint,
},
]);
});
});
describe('setSelectedProject', () => {
const id = 1;
it('commits SET_SELECTED_PROJECT mutation', () => {
testAction(
actions.setSelectedProject,
id,
{},
[
{
type: types.SET_SELECTED_PROJECT,
payload: id,
},
],
[
{
type: 'setBranchesEndpoint',
},
{
type: 'fetchBranches',
},
],
);
});
});
}); });
...@@ -18,4 +18,21 @@ describe('Commit form modal getters', () => { ...@@ -18,4 +18,21 @@ describe('Commit form modal getters', () => {
expect(getters.joinedBranches(state)).toEqual(branches.slice(1)); expect(getters.joinedBranches(state)).toEqual(branches.slice(1));
}); });
}); });
describe('sortedProjects', () => {
it('should sort projects with variable branches', () => {
const state = {
projects: mockData.mockProjects,
};
expect(getters.sortedProjects(state)).toEqual(mockData.mockProjects.sort());
});
it('should provide a uniq list of projects', () => {
const projects = ['_project_', '_project_', '_some_other_project'];
const state = { projects };
expect(getters.sortedProjects(state)).toEqual(projects.slice(1));
});
});
}); });
...@@ -35,6 +35,16 @@ describe('Commit form modal mutations', () => { ...@@ -35,6 +35,16 @@ describe('Commit form modal mutations', () => {
}); });
}); });
describe('SET_BRANCHES_ENDPOINT', () => {
it('should set branchesEndpoint', () => {
stateCopy = { branchesEndpoint: 'endpoint/1' };
mutations[types.SET_BRANCHES_ENDPOINT](stateCopy, 'endpoint/2');
expect(stateCopy.branchesEndpoint).toBe('endpoint/2');
});
});
describe('SET_BRANCH', () => { describe('SET_BRANCH', () => {
it('should set branch', () => { it('should set branch', () => {
stateCopy = { branch: '_master_' }; stateCopy = { branch: '_master_' };
...@@ -54,4 +64,14 @@ describe('Commit form modal mutations', () => { ...@@ -54,4 +64,14 @@ describe('Commit form modal mutations', () => {
expect(stateCopy.selectedBranch).toBe('_changed_branch_'); expect(stateCopy.selectedBranch).toBe('_changed_branch_');
}); });
}); });
describe('SET_SELECTED_PROJECT', () => {
it('should set targetProjectId', () => {
stateCopy = { targetProjectId: '_project_1_' };
mutations[types.SET_SELECTED_PROJECT](stateCopy, '_project_2_');
expect(stateCopy.targetProjectId).toBe('_project_2_');
});
});
}); });
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe CommitsHelper do RSpec.describe CommitsHelper do
include ProjectForksHelper
describe '#revert_commit_link' do describe '#revert_commit_link' do
context 'when current_user exists' do context 'when current_user exists' do
before do before do
...@@ -239,4 +241,21 @@ RSpec.describe CommitsHelper do ...@@ -239,4 +241,21 @@ RSpec.describe CommitsHelper do
end end
end end
end end
describe '#cherry_pick_projects_data' do
let(:project) { create(:project, :repository) }
let(:user) { create(:user, maintainer_projects: [project]) }
let!(:forked_project) { fork_project(project, user, { namespace: user.namespace, repository: true }) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
it 'returns data for cherry picking into a project' do
expect(helper.cherry_pick_projects_data(project)).to match_array([
{ id: project.id.to_s, name: project.full_path, refsUrl: refs_project_path(project) },
{ id: forked_project.id.to_s, name: forked_project.full_path, refsUrl: refs_project_path(forked_project) }
])
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment