Commit fc0d6e93 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch...

Merge branch '232577-populate-the-milestone-dropdown-combobox-on-the-release-edit-new-page-with-group-milestones' into 'master'

Resolve "Populate the milestone dropdown combobox on the Release edit/new page with Group milestones"

See merge request gitlab-org/gitlab!46027
parents 023b7230 49ae16b0
...@@ -39,6 +39,16 @@ export default { ...@@ -39,6 +39,16 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
groupId: {
type: String,
required: false,
default: '',
},
groupMilestonesAvailable: {
type: Boolean,
required: false,
default: false,
},
extraLinks: { extraLinks: {
type: Array, type: Array,
default: () => [], default: () => [],
...@@ -56,12 +66,13 @@ export default { ...@@ -56,12 +66,13 @@ export default {
noMilestone: s__('MilestoneCombobox|No milestone'), noMilestone: s__('MilestoneCombobox|No milestone'),
noResultsLabel: s__('MilestoneCombobox|No matching results'), noResultsLabel: s__('MilestoneCombobox|No matching results'),
searchMilestones: s__('MilestoneCombobox|Search Milestones'), searchMilestones: s__('MilestoneCombobox|Search Milestones'),
searhErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'), searchErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'),
projectMilestones: s__('MilestoneCombobox|Project milestones'), projectMilestones: s__('MilestoneCombobox|Project milestones'),
groupMilestones: s__('MilestoneCombobox|Group milestones'),
}, },
computed: { computed: {
...mapState(['matches', 'selectedMilestones']), ...mapState(['matches', 'selectedMilestones']),
...mapGetters(['isLoading']), ...mapGetters(['isLoading', 'groupMilestonesEnabled']),
selectedMilestonesLabel() { selectedMilestonesLabel() {
const { selectedMilestones } = this; const { selectedMilestones } = this;
const firstMilestoneName = selectedMilestones[0]; const firstMilestoneName = selectedMilestones[0];
...@@ -85,8 +96,14 @@ export default { ...@@ -85,8 +96,14 @@ export default {
this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error, this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error,
); );
}, },
showGroupMilestoneSection() {
return (
this.groupMilestonesEnabled &&
Boolean(this.matches.groupMilestones.totalCount > 0 || this.matches.groupMilestones.error)
);
},
showNoResults() { showNoResults() {
return !this.showProjectMilestoneSection; return !this.showProjectMilestoneSection && !this.showGroupMilestoneSection;
}, },
}, },
watch: { watch: {
...@@ -115,11 +132,15 @@ export default { ...@@ -115,11 +132,15 @@ export default {
}, SEARCH_DEBOUNCE_MS); }, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId); this.setProjectId(this.projectId);
this.setGroupId(this.groupId);
this.setGroupMilestonesAvailable(this.groupMilestonesAvailable);
this.fetchMilestones(); this.fetchMilestones();
}, },
methods: { methods: {
...mapActions([ ...mapActions([
'setProjectId', 'setProjectId',
'setGroupId',
'setGroupMilestonesAvailable',
'setSelectedMilestones', 'setSelectedMilestones',
'clearSelectedMilestones', 'clearSelectedMilestones',
'toggleMilestones', 'toggleMilestones',
...@@ -194,15 +215,28 @@ export default { ...@@ -194,15 +215,28 @@ export default {
</template> </template>
<template v-else> <template v-else>
<milestone-results-section <milestone-results-section
v-if="showProjectMilestoneSection"
:section-title="$options.translations.projectMilestones" :section-title="$options.translations.projectMilestones"
:total-count="matches.projectMilestones.totalCount" :total-count="matches.projectMilestones.totalCount"
:items="matches.projectMilestones.list" :items="matches.projectMilestones.list"
:selected-milestones="selectedMilestones" :selected-milestones="selectedMilestones"
:error="matches.projectMilestones.error" :error="matches.projectMilestones.error"
:error-message="$options.translations.searhErrorMessage" :error-message="$options.translations.searchErrorMessage"
data-testid="project-milestones-section" data-testid="project-milestones-section"
@selected="selectMilestone($event)" @selected="selectMilestone($event)"
/> />
<milestone-results-section
v-if="showGroupMilestoneSection"
:section-title="$options.translations.groupMilestones"
:total-count="matches.groupMilestones.totalCount"
:items="matches.groupMilestones.list"
:selected-milestones="selectedMilestones"
:error="matches.groupMilestones.error"
:error-message="$options.translations.searchErrorMessage"
data-testid="group-milestones-section"
@selected="selectMilestone($event)"
/>
</template> </template>
<gl-dropdown-item <gl-dropdown-item
v-for="(item, idx) in extraLinks" v-for="(item, idx) in extraLinks"
......
...@@ -2,6 +2,9 @@ import Api from '~/api'; ...@@ -2,6 +2,9 @@ import Api from '~/api';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId); export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId);
export const setGroupId = ({ commit }, groupId) => commit(types.SET_GROUP_ID, groupId);
export const setGroupMilestonesAvailable = ({ commit }, groupMilestonesAvailable) =>
commit(types.SET_GROUP_MILESTONES_AVAILABLE, groupMilestonesAvailable);
export const setSelectedMilestones = ({ commit }, selectedMilestones) => export const setSelectedMilestones = ({ commit }, selectedMilestones) =>
commit(types.SET_SELECTED_MILESTONES, selectedMilestones); commit(types.SET_SELECTED_MILESTONES, selectedMilestones);
...@@ -18,13 +21,23 @@ export const toggleMilestones = ({ commit, state }, selectedMilestone) => { ...@@ -18,13 +21,23 @@ export const toggleMilestones = ({ commit, state }, selectedMilestone) => {
} }
}; };
export const search = ({ dispatch, commit }, searchQuery) => { export const search = ({ dispatch, commit, getters }, searchQuery) => {
commit(types.SET_SEARCH_QUERY, searchQuery); commit(types.SET_SEARCH_QUERY, searchQuery);
dispatch('searchMilestones'); dispatch('searchProjectMilestones');
if (getters.groupMilestonesEnabled) {
dispatch('searchGroupMilestones');
}
};
export const fetchMilestones = ({ dispatch, getters }) => {
dispatch('fetchProjectMilestones');
if (getters.groupMilestonesEnabled) {
dispatch('fetchGroupMilestones');
}
}; };
export const fetchMilestones = ({ commit, state }) => { export const fetchProjectMilestones = ({ commit, state }) => {
commit(types.REQUEST_START); commit(types.REQUEST_START);
Api.projectMilestones(state.projectId) Api.projectMilestones(state.projectId)
...@@ -39,14 +52,29 @@ export const fetchMilestones = ({ commit, state }) => { ...@@ -39,14 +52,29 @@ export const fetchMilestones = ({ commit, state }) => {
}); });
}; };
export const searchMilestones = ({ commit, state }) => { export const fetchGroupMilestones = ({ commit, state }) => {
commit(types.REQUEST_START); commit(types.REQUEST_START);
Api.groupMilestones(state.groupId)
.then(response => {
commit(types.RECEIVE_GROUP_MILESTONES_SUCCESS, response);
})
.catch(error => {
commit(types.RECEIVE_GROUP_MILESTONES_ERROR, error);
})
.finally(() => {
commit(types.REQUEST_FINISH);
});
};
export const searchProjectMilestones = ({ commit, state }) => {
const options = { const options = {
search: state.searchQuery, search: state.searchQuery,
scope: 'milestones', scope: 'milestones',
}; };
commit(types.REQUEST_START);
Api.projectSearch(state.projectId, options) Api.projectSearch(state.projectId, options)
.then(response => { .then(response => {
commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response); commit(types.RECEIVE_PROJECT_MILESTONES_SUCCESS, response);
...@@ -58,3 +86,22 @@ export const searchMilestones = ({ commit, state }) => { ...@@ -58,3 +86,22 @@ export const searchMilestones = ({ commit, state }) => {
commit(types.REQUEST_FINISH); commit(types.REQUEST_FINISH);
}); });
}; };
export const searchGroupMilestones = ({ commit, state }) => {
const options = {
search: state.searchQuery,
};
commit(types.REQUEST_START);
Api.groupMilestones(state.groupId, options)
.then(response => {
commit(types.RECEIVE_GROUP_MILESTONES_SUCCESS, response);
})
.catch(error => {
commit(types.RECEIVE_GROUP_MILESTONES_ERROR, error);
})
.finally(() => {
commit(types.REQUEST_FINISH);
});
};
/** Returns `true` if there is at least one in-progress request */ /** Returns `true` if there is at least one in-progress request */
export const isLoading = ({ requestCount }) => requestCount > 0; export const isLoading = ({ requestCount }) => requestCount > 0;
/** Returns `true` if there is a group ID and group milestones are available */
export const groupMilestonesEnabled = ({ groupId, groupMilestonesAvailable }) =>
Boolean(groupId && groupMilestonesAvailable);
export const SET_PROJECT_ID = 'SET_PROJECT_ID'; export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const SET_GROUP_ID = 'SET_GROUP_ID';
export const SET_GROUP_MILESTONES_AVAILABLE = 'SET_GROUP_MILESTONES_AVAILABLE';
export const SET_SELECTED_MILESTONES = 'SET_SELECTED_MILESTONES'; export const SET_SELECTED_MILESTONES = 'SET_SELECTED_MILESTONES';
export const CLEAR_SELECTED_MILESTONES = 'CLEAR_SELECTED_MILESTONES'; export const CLEAR_SELECTED_MILESTONES = 'CLEAR_SELECTED_MILESTONES';
...@@ -12,3 +14,6 @@ export const REQUEST_FINISH = 'REQUEST_FINISH'; ...@@ -12,3 +14,6 @@ export const REQUEST_FINISH = 'REQUEST_FINISH';
export const RECEIVE_PROJECT_MILESTONES_SUCCESS = 'RECEIVE_PROJECT_MILESTONES_SUCCESS'; export const RECEIVE_PROJECT_MILESTONES_SUCCESS = 'RECEIVE_PROJECT_MILESTONES_SUCCESS';
export const RECEIVE_PROJECT_MILESTONES_ERROR = 'RECEIVE_PROJECT_MILESTONES_ERROR'; export const RECEIVE_PROJECT_MILESTONES_ERROR = 'RECEIVE_PROJECT_MILESTONES_ERROR';
export const RECEIVE_GROUP_MILESTONES_SUCCESS = 'RECEIVE_GROUP_MILESTONES_SUCCESS';
export const RECEIVE_GROUP_MILESTONES_ERROR = 'RECEIVE_GROUP_MILESTONES_ERROR';
import Vue from 'vue'; import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default { export default {
[types.SET_PROJECT_ID](state, projectId) { [types.SET_PROJECT_ID](state, projectId) {
state.projectId = projectId; state.projectId = projectId;
}, },
[types.SET_GROUP_ID](state, groupId) {
state.groupId = groupId;
},
[types.SET_GROUP_MILESTONES_AVAILABLE](state, groupMilestonesAvailable) {
state.groupMilestonesAvailable = groupMilestonesAvailable;
},
[types.SET_SELECTED_MILESTONES](state, selectedMilestones) { [types.SET_SELECTED_MILESTONES](state, selectedMilestones) {
Vue.set(state, 'selectedMilestones', selectedMilestones); Vue.set(state, 'selectedMilestones', selectedMilestones);
}, },
...@@ -32,7 +37,7 @@ export default { ...@@ -32,7 +37,7 @@ export default {
}, },
[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) { [types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) {
state.matches.projectMilestones = { state.matches.projectMilestones = {
list: convertObjectPropsToCamelCase(response.data).map(({ title }) => ({ title })), list: response.data.map(({ title }) => ({ title })),
totalCount: parseInt(response.headers['x-total'], 10), totalCount: parseInt(response.headers['x-total'], 10),
error: null, error: null,
}; };
...@@ -44,4 +49,18 @@ export default { ...@@ -44,4 +49,18 @@ export default {
error, error,
}; };
}, },
[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response) {
state.matches.groupMilestones = {
list: response.data.map(({ title }) => ({ title })),
totalCount: parseInt(response.headers['x-total'], 10),
error: null,
};
},
[types.RECEIVE_GROUP_MILESTONES_ERROR](state, error) {
state.matches.groupMilestones = {
list: [],
totalCount: 0,
error,
};
},
}; };
export default () => ({ export default () => ({
projectId: null, projectId: null,
groupId: null, groupId: null,
groupMilestonesAvailable: false,
searchQuery: '', searchQuery: '',
matches: { matches: {
projectMilestones: { projectMilestones: {
...@@ -8,6 +9,11 @@ export default () => ({ ...@@ -8,6 +9,11 @@ export default () => ({
totalCount: 0, totalCount: 0,
error: null, error: null,
}, },
groupMilestones: {
list: [],
totalCount: 0,
error: null,
},
}, },
selectedMilestones: [], selectedMilestones: [],
requestCount: 0, requestCount: 0,
......
...@@ -34,6 +34,8 @@ export default { ...@@ -34,6 +34,8 @@ export default {
'newMilestonePath', 'newMilestonePath',
'manageMilestonesPath', 'manageMilestonesPath',
'projectId', 'projectId',
'groupId',
'groupMilestonesAvailable',
]), ]),
...mapGetters('detail', ['isValid', 'isExistingRelease']), ...mapGetters('detail', ['isValid', 'isExistingRelease']),
showForm() { showForm() {
...@@ -141,6 +143,8 @@ export default { ...@@ -141,6 +143,8 @@ export default {
<milestone-combobox <milestone-combobox
v-model="releaseMilestones" v-model="releaseMilestones"
:project-id="projectId" :project-id="projectId"
:group-id="groupId"
:group-milestones-available="groupMilestonesAvailable"
:extra-links="milestoneComboboxExtraLinks" :extra-links="milestoneComboboxExtraLinks"
/> />
</div> </div>
......
export default ({ export default ({
projectId, projectId,
groupId,
groupMilestonesAvailable = false,
projectPath, projectPath,
markdownDocsPath, markdownDocsPath,
markdownPreviewPath, markdownPreviewPath,
...@@ -13,6 +15,8 @@ export default ({ ...@@ -13,6 +15,8 @@ export default ({
defaultBranch = null, defaultBranch = null,
}) => ({ }) => ({
projectId, projectId,
groupId,
groupMilestonesAvailable: Boolean(groupMilestonesAvailable),
projectPath, projectPath,
markdownDocsPath, markdownDocsPath,
markdownPreviewPath, markdownPreviewPath,
......
...@@ -51,11 +51,17 @@ module ReleasesHelper ...@@ -51,11 +51,17 @@ module ReleasesHelper
) )
end end
def group_milestone_project_releases_available?(project)
false
end
private private
def new_edit_pages_shared_data def new_edit_pages_shared_data
{ {
project_id: @project.id, project_id: @project.id,
group_id: @project.group&.id,
group_milestones_available: group_milestone_project_releases_available?(@project),
project_path: @project.full_path, project_path: @project.full_path,
markdown_preview_path: preview_markdown_path(@project), markdown_preview_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'), markdown_docs_path: help_page_path('user/markdown'),
...@@ -66,3 +72,5 @@ module ReleasesHelper ...@@ -66,3 +72,5 @@ module ReleasesHelper
} }
end end
end end
ReleasesHelper.prepend_if_ee('EE::ReleasesHelper')
...@@ -130,6 +130,8 @@ In the interface, to add release notes to an existing Git tag: ...@@ -130,6 +130,8 @@ In the interface, to add release notes to an existing Git tag:
You can associate a release with one or more [project milestones](../milestones/index.md#project-milestones-and-group-milestones). You can associate a release with one or more [project milestones](../milestones/index.md#project-milestones-and-group-milestones).
[GitLab Premium](https://about.gitlab.com/pricing/) customers can specify [group milestones](../milestones/index.md#project-milestones-and-group-milestones) to associate with a release.
You can do this in the user interface, or by including a `milestones` array in your request to You can do this in the user interface, or by including a `milestones` array in your request to
the [Releases API](../../../api/releases/index.md#create-a-release). the [Releases API](../../../api/releases/index.md#create-a-release).
......
# frozen_string_literal: true
module EE
module ReleasesHelper
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :group_milestone_project_releases_available?
def group_milestone_project_releases_available?(project)
project.feature_available?(:group_milestone_project_releases).to_s
end
end
end
---
title: Resolve Populate the milestone dropdown combobox on the Release edit/new page
with Group milestones
merge_request: 46027
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ReleasesHelper do
let(:project) { build(:project, namespace: create(:group)) }
let(:release) { create(:release, project: project) }
# rubocop: disable CodeReuse/ActiveRecord
before do
helper.instance_variable_set(:@project, project)
helper.instance_variable_set(:@release, release)
end
# rubocop: enable CodeReuse/ActiveRecord
describe '#group_milestone_project_releases_available?' do
subject { helper.data_for_edit_release_page[:group_milestones_available] }
context 'when group milestones association with project releases is enabled' do
before do
stub_licensed_features(group_milestone_project_releases: true)
end
it { is_expected.to eq("true") }
end
context 'when group milestones association with project releases is disabled' do
before do
stub_licensed_features(group_milestone_project_releases: false)
end
it { is_expected.to eq("false") }
end
end
end
...@@ -17388,6 +17388,9 @@ msgstr "" ...@@ -17388,6 +17388,9 @@ msgstr ""
msgid "MilestoneCombobox|An error occurred while searching for milestones" msgid "MilestoneCombobox|An error occurred while searching for milestones"
msgstr "" msgstr ""
msgid "MilestoneCombobox|Group milestones"
msgstr ""
msgid "MilestoneCombobox|Milestone" msgid "MilestoneCombobox|Milestone"
msgstr "" msgstr ""
......
export const milestones = [ export const projectMilestones = [
{ {
id: 41, id: 41,
iid: 6, iid: 6,
...@@ -79,4 +79,94 @@ export const milestones = [ ...@@ -79,4 +79,94 @@ export const milestones = [
}, },
]; ];
export default milestones; export const groupMilestones = [
{
id: 141,
iid: 16,
project_id: 8,
group_id: 12,
title: 'group-v0.1',
description: '',
state: 'active',
created_at: '2020-04-04T01:30:40.051Z',
updated_at: '2020-04-04T01:30:40.051Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/6',
},
{
id: 140,
iid: 15,
project_id: 8,
group_id: 12,
title: 'group-v4.0',
description: 'Laboriosam nisi sapiente dolores et magnam nobis ad earum.',
state: 'closed',
created_at: '2020-01-13T19:39:15.191Z',
updated_at: '2020-01-13T19:39:15.191Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/5',
},
{
id: 139,
iid: 14,
project_id: 8,
group_id: 12,
title: 'group-v3.0',
description: 'Necessitatibus illo alias et repellat dolorum assumenda ut.',
state: 'closed',
created_at: '2020-01-13T19:39:15.176Z',
updated_at: '2020-01-13T19:39:15.176Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/4',
},
{
id: 138,
iid: 13,
project_id: 8,
group_id: 12,
title: 'group-v2.0',
description: 'Doloribus qui repudiandae iste sit.',
state: 'closed',
created_at: '2020-01-13T19:39:15.161Z',
updated_at: '2020-01-13T19:39:15.161Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/3',
},
{
id: 137,
iid: 12,
project_id: 8,
group_id: 12,
title: 'group-v1.0',
description: 'Illo sint odio officia ea.',
state: 'closed',
created_at: '2020-01-13T19:39:15.146Z',
updated_at: '2020-01-13T19:39:15.146Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/2',
},
{
id: 136,
iid: 11,
project_id: 8,
group_id: 12,
title: 'group-v0.0',
description: 'Sed quae facilis deleniti at delectus assumenda nobis veritatis.',
state: 'active',
created_at: '2020-01-13T19:39:15.127Z',
updated_at: '2020-01-13T19:39:15.127Z',
due_date: null,
start_date: null,
web_url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/1',
},
];
export default {
projectMilestones,
groupMilestones,
};
...@@ -4,6 +4,7 @@ import * as actions from '~/milestones/stores/actions'; ...@@ -4,6 +4,7 @@ import * as actions from '~/milestones/stores/actions';
import * as types from '~/milestones/stores/mutation_types'; import * as types from '~/milestones/stores/mutation_types';
let mockProjectMilestonesReturnValue; let mockProjectMilestonesReturnValue;
let mockGroupMilestonesReturnValue;
let mockProjectSearchReturnValue; let mockProjectSearchReturnValue;
jest.mock('~/api', () => ({ jest.mock('~/api', () => ({
...@@ -13,6 +14,7 @@ jest.mock('~/api', () => ({ ...@@ -13,6 +14,7 @@ jest.mock('~/api', () => ({
default: { default: {
projectMilestones: () => mockProjectMilestonesReturnValue, projectMilestones: () => mockProjectMilestonesReturnValue,
projectSearch: () => mockProjectSearchReturnValue, projectSearch: () => mockProjectSearchReturnValue,
groupMilestones: () => mockGroupMilestonesReturnValue,
}, },
})); }));
...@@ -32,6 +34,24 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -32,6 +34,24 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
}); });
describe('setGroupId', () => {
it(`commits ${types.SET_GROUP_ID} with the new group ID`, () => {
const groupId = '123';
testAction(actions.setGroupId, groupId, state, [
{ type: types.SET_GROUP_ID, payload: groupId },
]);
});
});
describe('setGroupMilestonesAvailable', () => {
it(`commits ${types.SET_GROUP_MILESTONES_AVAILABLE} with the boolean indicating if group milestones are available (Premium)`, () => {
state.groupMilestonesAvailable = true;
testAction(actions.setGroupMilestonesAvailable, state.groupMilestonesAvailable, state, [
{ type: types.SET_GROUP_MILESTONES_AVAILABLE, payload: state.groupMilestonesAvailable },
]);
});
});
describe('setSelectedMilestones', () => { describe('setSelectedMilestones', () => {
it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => { it(`commits ${types.SET_SELECTED_MILESTONES} with the new selected milestones name`, () => {
const selectedMilestones = ['v1.2.3']; const selectedMilestones = ['v1.2.3'];
...@@ -66,19 +86,38 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -66,19 +86,38 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
describe('search', () => { describe('search', () => {
it(`commits ${types.SET_SEARCH_QUERY} with the new search query`, () => { describe('when project has license to add group milestones', () => {
const searchQuery = 'v1.0'; it(`commits ${types.SET_SEARCH_QUERY} with the new search query to search for project and group milestones`, () => {
testAction( const getters = {
actions.search, groupMilestonesEnabled: () => true,
searchQuery, };
state,
[{ type: types.SET_SEARCH_QUERY, payload: searchQuery }], const searchQuery = 'v1.0';
[{ type: 'searchMilestones' }], testAction(
); actions.search,
searchQuery,
{ ...state, ...getters },
[{ type: types.SET_SEARCH_QUERY, payload: searchQuery }],
[{ type: 'searchProjectMilestones' }, { type: 'searchGroupMilestones' }],
);
});
});
describe('when project does not have license to add group milestones', () => {
it(`commits ${types.SET_SEARCH_QUERY} with the new search query to search for project milestones`, () => {
const searchQuery = 'v1.0';
testAction(
actions.search,
searchQuery,
state,
[{ type: types.SET_SEARCH_QUERY, payload: searchQuery }],
[{ type: 'searchProjectMilestones' }],
);
});
}); });
}); });
describe('searchMilestones', () => { describe('searchProjectMilestones', () => {
describe('when the search is successful', () => { describe('when the search is successful', () => {
const projectSearchApiResponse = { data: [{ title: 'v1.0' }] }; const projectSearchApiResponse = { data: [{ title: 'v1.0' }] };
...@@ -87,7 +126,7 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -87,7 +126,7 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchMilestones, undefined, state, [ return testAction(actions.searchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START }, { type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse }, { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectSearchApiResponse },
{ type: types.REQUEST_FINISH }, { type: types.REQUEST_FINISH },
...@@ -103,7 +142,7 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -103,7 +142,7 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchMilestones, undefined, state, [ return testAction(actions.searchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START }, { type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error }, { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH }, { type: types.REQUEST_FINISH },
...@@ -112,7 +151,71 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -112,7 +151,71 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
}); });
describe('searchGroupMilestones', () => {
describe('when the search is successful', () => {
const groupSearchApiResponse = { data: [{ title: 'group-v1.0' }] };
beforeEach(() => {
mockGroupMilestonesReturnValue = Promise.resolve(groupSearchApiResponse);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchGroupMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_GROUP_MILESTONES_SUCCESS, payload: groupSearchApiResponse },
{ type: types.REQUEST_FINISH },
]);
});
});
describe('when the search fails', () => {
const error = new Error('Something went wrong!');
beforeEach(() => {
mockGroupMilestonesReturnValue = Promise.reject(error);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.searchGroupMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_GROUP_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH },
]);
});
});
});
describe('fetchMilestones', () => { describe('fetchMilestones', () => {
describe('when project has license to add group milestones', () => {
it(`dispatchs fetchProjectMilestones and fetchGroupMilestones`, () => {
const getters = {
groupMilestonesEnabled: () => true,
};
testAction(
actions.fetchMilestones,
undefined,
{ ...state, ...getters },
[],
[{ type: 'fetchProjectMilestones' }, { type: 'fetchGroupMilestones' }],
);
});
});
describe('when project does not have license to add group milestones', () => {
it(`dispatchs fetchProjectMilestones`, () => {
testAction(
actions.fetchMilestones,
undefined,
state,
[],
[{ type: 'fetchProjectMilestones' }],
);
});
});
});
describe('fetchProjectMilestones', () => {
describe('when the fetch is successful', () => { describe('when the fetch is successful', () => {
const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] }; const projectMilestonesApiResponse = { data: [{ title: 'v1.0' }] };
...@@ -121,7 +224,7 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -121,7 +224,7 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => { it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchMilestones, undefined, state, [ return testAction(actions.fetchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START }, { type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse }, { type: types.RECEIVE_PROJECT_MILESTONES_SUCCESS, payload: projectMilestonesApiResponse },
{ type: types.REQUEST_FINISH }, { type: types.REQUEST_FINISH },
...@@ -137,7 +240,7 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -137,7 +240,7 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => { it(`commits ${types.REQUEST_START}, ${types.RECEIVE_PROJECT_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchMilestones, undefined, state, [ return testAction(actions.fetchProjectMilestones, undefined, state, [
{ type: types.REQUEST_START }, { type: types.REQUEST_START },
{ type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error }, { type: types.RECEIVE_PROJECT_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH }, { type: types.REQUEST_FINISH },
...@@ -145,4 +248,38 @@ describe('Milestone combobox Vuex store actions', () => { ...@@ -145,4 +248,38 @@ describe('Milestone combobox Vuex store actions', () => {
}); });
}); });
}); });
describe('fetchGroupMilestones', () => {
describe('when the fetch is successful', () => {
const groupMilestonesApiResponse = { data: [{ title: 'group-v1.0' }] };
beforeEach(() => {
mockGroupMilestonesReturnValue = Promise.resolve(groupMilestonesApiResponse);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_SUCCESS} with the response from the API, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchGroupMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_GROUP_MILESTONES_SUCCESS, payload: groupMilestonesApiResponse },
{ type: types.REQUEST_FINISH },
]);
});
});
describe('when the fetch fails', () => {
const error = new Error('Something went wrong!');
beforeEach(() => {
mockGroupMilestonesReturnValue = Promise.reject(error);
});
it(`commits ${types.REQUEST_START}, ${types.RECEIVE_GROUP_MILESTONES_ERROR} with the error object, and ${types.REQUEST_FINISH}`, () => {
return testAction(actions.fetchGroupMilestones, undefined, state, [
{ type: types.REQUEST_START },
{ type: types.RECEIVE_GROUP_MILESTONES_ERROR, payload: error },
{ type: types.REQUEST_FINISH },
]);
});
});
});
}); });
...@@ -12,4 +12,22 @@ describe('Milestone comboxbox Vuex store getters', () => { ...@@ -12,4 +12,22 @@ describe('Milestone comboxbox Vuex store getters', () => {
expect(getters.isLoading({ requestCount })).toBe(isLoading); expect(getters.isLoading({ requestCount })).toBe(isLoading);
}); });
}); });
describe('groupMilestonesEnabled', () => {
it.each`
groupId | groupMilestonesAvailable | groupMilestonesEnabled
${'1'} | ${true} | ${true}
${'1'} | ${false} | ${false}
${''} | ${true} | ${false}
${''} | ${false} | ${false}
${null} | ${true} | ${false}
`(
'returns true when groupId is a truthy string and groupMilestonesAvailable is true',
({ groupId, groupMilestonesAvailable, groupMilestonesEnabled }) => {
expect(getters.groupMilestonesEnabled({ groupId, groupMilestonesAvailable })).toBe(
groupMilestonesEnabled,
);
},
);
});
}); });
...@@ -14,6 +14,7 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -14,6 +14,7 @@ describe('Milestones combobox Vuex store mutations', () => {
expect(state).toEqual({ expect(state).toEqual({
projectId: null, projectId: null,
groupId: null, groupId: null,
groupMilestonesAvailable: false,
searchQuery: '', searchQuery: '',
matches: { matches: {
projectMilestones: { projectMilestones: {
...@@ -21,6 +22,11 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -21,6 +22,11 @@ describe('Milestones combobox Vuex store mutations', () => {
totalCount: 0, totalCount: 0,
error: null, error: null,
}, },
groupMilestones: {
list: [],
totalCount: 0,
error: null,
},
}, },
selectedMilestones: [], selectedMilestones: [],
requestCount: 0, requestCount: 0,
...@@ -37,6 +43,24 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -37,6 +43,24 @@ describe('Milestones combobox Vuex store mutations', () => {
}); });
}); });
describe(`${types.SET_GROUP_ID}`, () => {
it('updates the group ID', () => {
const newGroupId = '8';
mutations[types.SET_GROUP_ID](state, newGroupId);
expect(state.groupId).toBe(newGroupId);
});
});
describe(`${types.SET_GROUP_MILESTONES_AVAILABLE}`, () => {
it('sets boolean indicating if group milestones are available', () => {
const groupMilestonesAvailable = true;
mutations[types.SET_GROUP_MILESTONES_AVAILABLE](state, groupMilestonesAvailable);
expect(state.groupMilestonesAvailable).toBe(groupMilestonesAvailable);
});
});
describe(`${types.SET_SELECTED_MILESTONES}`, () => { describe(`${types.SET_SELECTED_MILESTONES}`, () => {
it('sets the selected milestones', () => { it('sets the selected milestones', () => {
const selectedMilestones = ['v1.2.3']; const selectedMilestones = ['v1.2.3'];
...@@ -60,7 +84,7 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -60,7 +84,7 @@ describe('Milestones combobox Vuex store mutations', () => {
}); });
}); });
describe(`${types.ADD_SELECTED_MILESTONESs}`, () => { describe(`${types.ADD_SELECTED_MILESTONES}`, () => {
it('adds the selected milestones', () => { it('adds the selected milestones', () => {
const selectedMilestone = 'v1.2.3'; const selectedMilestone = 'v1.2.3';
mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone); mutations[types.ADD_SELECTED_MILESTONE](state, selectedMilestone);
...@@ -170,4 +194,57 @@ describe('Milestones combobox Vuex store mutations', () => { ...@@ -170,4 +194,57 @@ describe('Milestones combobox Vuex store mutations', () => {
}); });
}); });
}); });
describe(`${types.RECEIVE_GROUP_MILESTONES_SUCCESS}`, () => {
it('updates state.matches.groupMilestones based on the provided API response', () => {
const response = {
data: [
{
title: 'group-0.1',
},
{
title: 'group-0.2',
},
],
headers: {
'x-total': 2,
},
};
mutations[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response);
expect(state.matches.groupMilestones).toEqual({
list: [
{
title: 'group-0.1',
},
{
title: 'group-0.2',
},
],
error: null,
totalCount: 2,
});
});
describe(`${types.RECEIVE_GROUP_MILESTONES_ERROR}`, () => {
it('updates state.matches.groupMilestones to an empty state with the error object', () => {
const error = new Error('Something went wrong!');
state.matches.groupMilestones = {
list: [{ title: 'group-0.1' }],
totalCount: 1,
error: null,
};
mutations[types.RECEIVE_GROUP_MILESTONES_ERROR](state, error);
expect(state.matches.groupMilestones).toEqual({
list: [],
totalCount: 0,
error,
});
});
});
});
}); });
...@@ -27,6 +27,8 @@ describe('Release edit/new component', () => { ...@@ -27,6 +27,8 @@ describe('Release edit/new component', () => {
updateReleaseApiDocsPath: 'path/to/update/release/api/docs', updateReleaseApiDocsPath: 'path/to/update/release/api/docs',
releasesPagePath: 'path/to/releases/page', releasesPagePath: 'path/to/releases/page',
projectId: '8', projectId: '8',
groupId: '42',
groupMilestonesAvailable: true,
}; };
actions = { actions = {
......
...@@ -64,6 +64,8 @@ RSpec.describe ReleasesHelper do ...@@ -64,6 +64,8 @@ RSpec.describe ReleasesHelper do
describe '#data_for_edit_release_page' do describe '#data_for_edit_release_page' do
it 'has the needed data to display the "edit release" page' do it 'has the needed data to display the "edit release" page' do
keys = %i(project_id keys = %i(project_id
group_id
group_milestones_available
project_path project_path
tag_name tag_name
markdown_preview_path markdown_preview_path
...@@ -81,6 +83,8 @@ RSpec.describe ReleasesHelper do ...@@ -81,6 +83,8 @@ RSpec.describe ReleasesHelper do
describe '#data_for_new_release_page' do describe '#data_for_new_release_page' do
it 'has the needed data to display the "new release" page' do it 'has the needed data to display the "new release" page' do
keys = %i(project_id keys = %i(project_id
group_id
group_milestones_available
project_path project_path
releases_page_path releases_page_path
markdown_preview_path markdown_preview_path
......
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