Commit 35f17fe0 authored by Nathan Friend's avatar Nathan Friend Committed by Andrew Fontaine

Update Edit Release page to support creation

This commit updates the Edit Release page to additionally support the
creation of new releases.
parent 611b819d
...@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -3,7 +3,6 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui'; import { GlButton, GlFormInput, GlFormGroup } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants'; import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils'; import { getParameterByName } from '~/lib/utils/common_utils';
import AssetLinksForm from './asset_links_form.vue'; import AssetLinksForm from './asset_links_form.vue';
...@@ -22,9 +21,6 @@ export default { ...@@ -22,9 +21,6 @@ export default {
MilestoneCombobox, MilestoneCombobox,
TagField, TagField,
}, },
directives: {
autofocusonshow,
},
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
computed: { computed: {
...mapState('detail', [ ...mapState('detail', [
...@@ -40,9 +36,9 @@ export default { ...@@ -40,9 +36,9 @@ export default {
'manageMilestonesPath', 'manageMilestonesPath',
'projectId', 'projectId',
]), ]),
...mapGetters('detail', ['isValid']), ...mapGetters('detail', ['isValid', 'isExistingRelease']),
showForm() { showForm() {
return !this.isFetchingRelease && !this.fetchError; return Boolean(!this.isFetchingRelease && !this.fetchError && this.release);
}, },
subtitleText() { subtitleText() {
return sprintf( return sprintf(
...@@ -86,6 +82,9 @@ export default { ...@@ -86,6 +82,9 @@ export default {
showAssetLinksForm() { showAssetLinksForm() {
return this.glFeatures.releaseAssetLinkEditing; return this.glFeatures.releaseAssetLinkEditing;
}, },
saveButtonLabel() {
return this.isExistingRelease ? __('Save changes') : __('Create release');
},
isSaveChangesDisabled() { isSaveChangesDisabled() {
return this.isUpdatingRelease || !this.isValid; return this.isUpdatingRelease || !this.isValid;
}, },
...@@ -102,13 +101,17 @@ export default { ...@@ -102,13 +101,17 @@ export default {
]; ];
}, },
}, },
created() { mounted() {
this.fetchRelease(); // eslint-disable-next-line promise/catch-or-return
this.initializeRelease().then(() => {
// Focus the first non-disabled input element
this.$el.querySelector('input:enabled').focus();
});
}, },
methods: { methods: {
...mapActions('detail', [ ...mapActions('detail', [
'fetchRelease', 'initializeRelease',
'updateRelease', 'saveRelease',
'updateReleaseTitle', 'updateReleaseTitle',
'updateReleaseNotes', 'updateReleaseNotes',
'updateReleaseMilestones', 'updateReleaseMilestones',
...@@ -119,7 +122,7 @@ export default { ...@@ -119,7 +122,7 @@ export default {
<template> <template>
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<p class="pt-3 js-subtitle-text" v-html="subtitleText"></p> <p class="pt-3 js-subtitle-text" v-html="subtitleText"></p>
<form v-if="showForm" @submit.prevent="updateRelease()"> <form v-if="showForm" @submit.prevent="saveRelease()">
<tag-field /> <tag-field />
<gl-form-group> <gl-form-group>
<label for="release-title">{{ __('Release title') }}</label> <label for="release-title">{{ __('Release title') }}</label>
...@@ -127,8 +130,6 @@ export default { ...@@ -127,8 +130,6 @@ export default {
id="release-title" id="release-title"
ref="releaseTitleInput" ref="releaseTitleInput"
v-model="releaseTitle" v-model="releaseTitle"
v-autofocusonshow
autofocus
type="text" type="text"
class="form-control" class="form-control"
/> />
...@@ -162,8 +163,8 @@ export default { ...@@ -162,8 +163,8 @@ export default {
data-supports-quick-actions="false" data-supports-quick-actions="false"
:aria-label="__('Release notes')" :aria-label="__('Release notes')"
:placeholder="__('Write your release notes or drag your files here…')" :placeholder="__('Write your release notes or drag your files here…')"
@keydown.meta.enter="updateRelease()" @keydown.meta.enter="saveRelease()"
@keydown.ctrl.enter="updateRelease()" @keydown.ctrl.enter="saveRelease()"
></textarea> ></textarea>
</template> </template>
</markdown-field> </markdown-field>
...@@ -178,10 +179,11 @@ export default { ...@@ -178,10 +179,11 @@ export default {
category="primary" category="primary"
variant="success" variant="success"
type="submit" type="submit"
:aria-label="__('Save changes')"
:disabled="isSaveChangesDisabled" :disabled="isSaveChangesDisabled"
>{{ __('Save changes') }}</gl-button data-testid="submit-button"
> >
{{ saveButtonLabel }}
</gl-button>
<gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button> <gl-button :href="cancelPath" class="js-cancel-button">{{ __('Cancel') }}</gl-button>
</div> </div>
</form> </form>
......
...@@ -3,76 +3,114 @@ import api from '~/api'; ...@@ -3,76 +3,114 @@ import api from '~/api';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase, export const initializeRelease = ({ commit, dispatch, getters }) => {
} from '~/lib/utils/common_utils'; if (getters.isExistingRelease) {
// When editing an existing release,
export const requestRelease = ({ commit }) => commit(types.REQUEST_RELEASE); // fetch the release object from the API
export const receiveReleaseSuccess = ({ commit }, data) => return dispatch('fetchRelease');
commit(types.RECEIVE_RELEASE_SUCCESS, data); }
export const receiveReleaseError = ({ commit }, error) => {
commit(types.RECEIVE_RELEASE_ERROR, error); // When creating a new release, initialize the
createFlash(s__('Release|Something went wrong while getting the release details')); // store with an empty release object
commit(types.INITIALIZE_EMPTY_RELEASE);
return Promise.resolve();
}; };
export const fetchRelease = ({ dispatch, state }) => { export const fetchRelease = ({ commit, state }) => {
dispatch('requestRelease'); commit(types.REQUEST_RELEASE);
return api return api
.release(state.projectId, state.tagName) .release(state.projectId, state.tagName)
.then(({ data }) => { .then(({ data }) => {
const release = { commit(types.RECEIVE_RELEASE_SUCCESS, apiJsonToRelease(data));
...data,
milestones: data.milestones || [],
};
dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true }));
}) })
.catch(error => { .catch(error => {
dispatch('receiveReleaseError', error); commit(types.RECEIVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while getting the release details'));
}); });
}; };
export const updateReleaseTagName = ({ commit }, tagName) => export const updateReleaseTagName = ({ commit }, tagName) =>
commit(types.UPDATE_RELEASE_TAG_NAME, tagName); commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
export const updateCreateFrom = ({ commit }, createFrom) => export const updateCreateFrom = ({ commit }, createFrom) =>
commit(types.UPDATE_CREATE_FROM, createFrom); commit(types.UPDATE_CREATE_FROM, createFrom);
export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title); export const updateReleaseTitle = ({ commit }, title) => commit(types.UPDATE_RELEASE_TITLE, title);
export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes); export const updateReleaseNotes = ({ commit }, notes) => commit(types.UPDATE_RELEASE_NOTES, notes);
export const updateReleaseMilestones = ({ commit }, milestones) => export const updateReleaseMilestones = ({ commit }, milestones) =>
commit(types.UPDATE_RELEASE_MILESTONES, milestones); commit(types.UPDATE_RELEASE_MILESTONES, milestones);
export const requestUpdateRelease = ({ commit }) => commit(types.REQUEST_UPDATE_RELEASE); export const addEmptyAssetLink = ({ commit }) => {
export const receiveUpdateReleaseSuccess = ({ commit, state, rootState }) => { commit(types.ADD_EMPTY_ASSET_LINK);
commit(types.RECEIVE_UPDATE_RELEASE_SUCCESS);
redirectTo(
rootState.featureFlags.releaseShowPage ? state.release._links.self : state.releasesPagePath,
);
}; };
export const receiveUpdateReleaseError = ({ commit }, error) => {
commit(types.RECEIVE_UPDATE_RELEASE_ERROR, error); export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
createFlash(s__('Release|Something went wrong while saving the release details')); commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
};
export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
};
export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
};
export const removeAssetLink = ({ commit }, linkIdToRemove) => {
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
}; };
export const updateRelease = ({ dispatch, state, getters }) => { export const receiveSaveReleaseSuccess = ({ commit, state, rootState }, release) => {
dispatch('requestUpdateRelease'); commit(types.RECEIVE_SAVE_RELEASE_SUCCESS);
redirectTo(rootState.featureFlags.releaseShowPage ? release._links.self : state.releasesPagePath);
};
const { release } = state; export const saveRelease = ({ commit, dispatch, getters }) => {
const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : []; commit(types.REQUEST_SAVE_RELEASE);
const updatedRelease = convertObjectPropsToSnakeCase( dispatch(getters.isExistingRelease ? 'updateRelease' : 'createRelease');
};
export const createRelease = ({ commit, dispatch, state, getters }) => {
const apiJson = releaseToApiJson(
{ {
name: release.name, ...state.release,
description: release.description, assets: {
milestones, links: getters.releaseLinksToCreate,
},
}, },
{ deep: true }, state.createFrom,
); );
return api
.createRelease(state.projectId, apiJson)
.then(({ data }) => {
dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(data));
})
.catch(error => {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while creating a new release'));
});
};
export const updateRelease = ({ commit, dispatch, state, getters }) => {
const apiJson = releaseToApiJson({
...state.release,
assets: {
links: getters.releaseLinksToCreate,
},
});
let updatedRelease = null;
return ( return (
api api
.updateRelease(state.projectId, state.tagName, updatedRelease) .updateRelease(state.projectId, state.tagName, apiJson)
/** /**
* Currently, we delete all existing links and then * Currently, we delete all existing links and then
...@@ -90,54 +128,31 @@ export const updateRelease = ({ dispatch, state, getters }) => { ...@@ -90,54 +128,31 @@ export const updateRelease = ({ dispatch, state, getters }) => {
* https://gitlab.com/gitlab-org/gitlab/-/issues/208702 * https://gitlab.com/gitlab-org/gitlab/-/issues/208702
* is closed. * is closed.
*/ */
.then(({ data }) => {
// Save this response since we need it later in the Promise chain
updatedRelease = data;
.then(() => {
// Delete all links currently associated with this Release // Delete all links currently associated with this Release
return Promise.all( return Promise.all(
getters.releaseLinksToDelete.map(l => getters.releaseLinksToDelete.map(l =>
api.deleteReleaseLink(state.projectId, release.tagName, l.id), api.deleteReleaseLink(state.projectId, state.release.tagName, l.id),
), ),
); );
}) })
.then(() => { .then(() => {
// Create a new link for each link in the form // Create a new link for each link in the form
return Promise.all( return Promise.all(
getters.releaseLinksToCreate.map(l => apiJson.assets.links.map(l =>
api.createReleaseLink( api.createReleaseLink(state.projectId, state.release.tagName, l),
state.projectId,
release.tagName,
convertObjectPropsToSnakeCase(l, { deep: true }),
),
), ),
); );
}) })
.then(() => dispatch('receiveUpdateReleaseSuccess')) .then(() => {
dispatch('receiveSaveReleaseSuccess', apiJsonToRelease(updatedRelease));
})
.catch(error => { .catch(error => {
dispatch('receiveUpdateReleaseError', error); commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
createFlash(s__('Release|Something went wrong while saving the release details'));
}) })
); );
}; };
export const navigateToReleasesPage = ({ state }) => {
redirectTo(state.releasesPagePath);
};
export const addEmptyAssetLink = ({ commit }) => {
commit(types.ADD_EMPTY_ASSET_LINK);
};
export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
};
export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
};
export const updateAssetLinkType = ({ commit }, { linkIdToUpdate, newType }) => {
commit(types.UPDATE_ASSET_LINK_TYPE, { linkIdToUpdate, newType });
};
export const removeAssetLink = ({ commit }, linkIdToRemove) => {
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
};
...@@ -6,7 +6,7 @@ import { hasContent } from '~/lib/utils/text_utility'; ...@@ -6,7 +6,7 @@ import { hasContent } from '~/lib/utils/text_utility';
* `false` if the app is creating a new release. * `false` if the app is creating a new release.
*/ */
export const isExistingRelease = state => { export const isExistingRelease = state => {
return Boolean(state.originalRelease); return Boolean(state.tagName);
}; };
/** /**
......
export const INITIALIZE_EMPTY_RELEASE = 'INITIALIZE_EMPTY_RELEASE';
export const REQUEST_RELEASE = 'REQUEST_RELEASE'; export const REQUEST_RELEASE = 'REQUEST_RELEASE';
export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS'; export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR'; export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
...@@ -8,9 +10,9 @@ export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE'; ...@@ -8,9 +10,9 @@ export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES'; export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES'; export const UPDATE_RELEASE_MILESTONES = 'UPDATE_RELEASE_MILESTONES';
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE'; export const REQUEST_SAVE_RELEASE = 'REQUEST_SAVE_RELEASE';
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS'; export const RECEIVE_SAVE_RELEASE_SUCCESS = 'RECEIVE_SAVE_RELEASE_SUCCESS';
export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR'; export const RECEIVE_SAVE_RELEASE_ERROR = 'RECEIVE_SAVE_RELEASE_ERROR';
export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK'; export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL'; export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
......
...@@ -7,6 +7,18 @@ const findReleaseLink = (release, id) => { ...@@ -7,6 +7,18 @@ const findReleaseLink = (release, id) => {
}; };
export default { export default {
[types.INITIALIZE_EMPTY_RELEASE](state) {
state.release = {
tagName: null,
name: '',
description: '',
milestones: [],
assets: {
links: [],
},
};
},
[types.REQUEST_RELEASE](state) { [types.REQUEST_RELEASE](state) {
state.isFetchingRelease = true; state.isFetchingRelease = true;
}, },
...@@ -39,14 +51,14 @@ export default { ...@@ -39,14 +51,14 @@ export default {
state.release.milestones = milestones; state.release.milestones = milestones;
}, },
[types.REQUEST_UPDATE_RELEASE](state) { [types.REQUEST_SAVE_RELEASE](state) {
state.isUpdatingRelease = true; state.isUpdatingRelease = true;
}, },
[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state) { [types.RECEIVE_SAVE_RELEASE_SUCCESS](state) {
state.updateError = undefined; state.updateError = undefined;
state.isUpdatingRelease = false; state.isUpdatingRelease = false;
}, },
[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error) { [types.RECEIVE_SAVE_RELEASE_ERROR](state, error) {
state.updateError = error; state.updateError = error;
state.isUpdatingRelease = false; state.isUpdatingRelease = false;
}, },
......
import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
} from '~/lib/utils/common_utils';
/**
* Converts a release object into a JSON object that can sent to the public
* API to create or update a release.
* @param {Object} release The release object to convert
* @param {string} createFrom The ref to create a new tag from, if necessary
*/
export const releaseToApiJson = (release, createFrom = null) => {
const milestones = release.milestones ? release.milestones.map(milestone => milestone.title) : [];
return convertObjectPropsToSnakeCase(
{
tagName: release.tagName,
ref: createFrom,
name: release.name,
description: release.description,
milestones,
assets: release.assets,
},
{ deep: true },
);
};
/**
* Converts a JSON release object returned by the Release API
* into the structure this Vue application can work with.
* @param {Object} json The JSON object received from the release API
*/
export const apiJsonToRelease = json => {
const release = convertObjectPropsToCamelCase(json, { deep: true });
release.milestones = release.milestones || [];
return release;
};
...@@ -7093,6 +7093,9 @@ msgstr "" ...@@ -7093,6 +7093,9 @@ msgstr ""
msgid "Create project label" msgid "Create project label"
msgstr "" msgstr ""
msgid "Create release"
msgstr ""
msgid "Create requirement" msgid "Create requirement"
msgstr "" msgstr ""
...@@ -20099,6 +20102,9 @@ msgstr "" ...@@ -20099,6 +20102,9 @@ msgstr ""
msgid "Releases|New Release" msgid "Releases|New Release"
msgstr "" msgstr ""
msgid "Release|Something went wrong while creating a new release"
msgstr ""
msgid "Release|Something went wrong while getting the release details" msgid "Release|Something went wrong while getting the release details"
msgstr "" msgstr ""
......
...@@ -27,8 +27,8 @@ describe('Release edit/new component', () => { ...@@ -27,8 +27,8 @@ describe('Release edit/new component', () => {
}; };
actions = { actions = {
fetchRelease: jest.fn(), initializeRelease: jest.fn(),
updateRelease: jest.fn(), saveRelease: jest.fn(),
addEmptyAssetLink: jest.fn(), addEmptyAssetLink: jest.fn(),
}; };
...@@ -64,6 +64,8 @@ describe('Release edit/new component', () => { ...@@ -64,6 +64,8 @@ describe('Release edit/new component', () => {
glFeatures: featureFlags, glFeatures: featureFlags,
}, },
}); });
wrapper.element.querySelectorAll('input').forEach(input => jest.spyOn(input, 'focus'));
}; };
beforeEach(() => { beforeEach(() => {
...@@ -87,8 +89,18 @@ describe('Release edit/new component', () => { ...@@ -87,8 +89,18 @@ describe('Release edit/new component', () => {
factory(); factory();
}); });
it('calls fetchRelease when the component is created', () => { it('calls initializeRelease when the component is created', () => {
expect(actions.fetchRelease).toHaveBeenCalledTimes(1); expect(actions.initializeRelease).toHaveBeenCalledTimes(1);
});
it('focuses the first non-disabled input element once the page is shown', () => {
const firstEnabledInput = wrapper.element.querySelector('input:enabled');
const allInputs = wrapper.element.querySelectorAll('input');
allInputs.forEach(input => {
const expectedFocusCalls = input === firstEnabledInput ? 1 : 0;
expect(input.focus).toHaveBeenCalledTimes(expectedFocusCalls);
});
}); });
it('renders the description text at the top of the page', () => { it('renders the description text at the top of the page', () => {
...@@ -109,9 +121,9 @@ describe('Release edit/new component', () => { ...@@ -109,9 +121,9 @@ describe('Release edit/new component', () => {
expect(findSubmitButton().attributes('type')).toBe('submit'); expect(findSubmitButton().attributes('type')).toBe('submit');
}); });
it('calls updateRelease when the form is submitted', () => { it('calls saveRelease when the form is submitted', () => {
wrapper.find('form').trigger('submit'); wrapper.find('form').trigger('submit');
expect(actions.updateRelease).toHaveBeenCalledTimes(1); expect(actions.saveRelease).toHaveBeenCalledTimes(1);
}); });
}); });
...@@ -143,6 +155,34 @@ describe('Release edit/new component', () => { ...@@ -143,6 +155,34 @@ describe('Release edit/new component', () => {
}); });
}); });
describe('when creating a new release', () => {
beforeEach(() => {
factory({
store: {
modules: {
detail: {
getters: {
isExistingRelease: () => false,
},
},
},
},
});
});
it('renders the submit button with the text "Create release"', () => {
expect(findSubmitButton().text()).toBe('Create release');
});
});
describe('when editing an existing release', () => {
beforeEach(factory);
it('renders the submit button with the text "Save changes"', () => {
expect(findSubmitButton().text()).toBe('Save changes');
});
});
describe('asset links form', () => { describe('asset links form', () => {
const findAssetLinksForm = () => wrapper.find(AssetLinksForm); const findAssetLinksForm = () => wrapper.find(AssetLinksForm);
......
...@@ -9,14 +9,14 @@ describe('releases/components/tag_field', () => { ...@@ -9,14 +9,14 @@ describe('releases/components/tag_field', () => {
let store; let store;
let wrapper; let wrapper;
const createComponent = ({ originalRelease }) => { const createComponent = ({ tagName }) => {
store = createStore({ store = createStore({
modules: { modules: {
detail: createDetailModule({}), detail: createDetailModule({}),
}, },
}); });
store.state.detail.originalRelease = originalRelease; store.state.detail.tagName = tagName;
wrapper = shallowMount(TagField, { store }); wrapper = shallowMount(TagField, { store });
}; };
...@@ -31,8 +31,7 @@ describe('releases/components/tag_field', () => { ...@@ -31,8 +31,7 @@ describe('releases/components/tag_field', () => {
describe('when an existing release is being edited', () => { describe('when an existing release is being edited', () => {
beforeEach(() => { beforeEach(() => {
const originalRelease = { name: 'Version 1.0' }; createComponent({ tagName: 'v1.0' });
createComponent({ originalRelease });
}); });
it('renders the TagFieldExisting component', () => { it('renders the TagFieldExisting component', () => {
...@@ -46,7 +45,7 @@ describe('releases/components/tag_field', () => { ...@@ -46,7 +45,7 @@ describe('releases/components/tag_field', () => {
describe('when a new release is being created', () => { describe('when a new release is being created', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ originalRelease: null }); createComponent({ tagName: null });
}); });
it('renders the TagFieldNew component', () => { it('renders the TagFieldNew component', () => {
......
...@@ -3,13 +3,13 @@ import * as getters from '~/releases/stores/modules/detail/getters'; ...@@ -3,13 +3,13 @@ import * as getters from '~/releases/stores/modules/detail/getters';
describe('Release detail getters', () => { describe('Release detail getters', () => {
describe('isExistingRelease', () => { describe('isExistingRelease', () => {
it('returns true if the release is an existing release that already exists in the database', () => { it('returns true if the release is an existing release that already exists in the database', () => {
const state = { originalRelease: { name: 'The first release' } }; const state = { tagName: 'test-tag-name' };
expect(getters.isExistingRelease(state)).toBe(true); expect(getters.isExistingRelease(state)).toBe(true);
}); });
it('returns false if the release is a new release that has not yet been saved to the database', () => { it('returns false if the release is a new release that has not yet been saved to the database', () => {
const state = { originalRelease: null }; const state = { tagName: null };
expect(getters.isExistingRelease(state)).toBe(false); expect(getters.isExistingRelease(state)).toBe(false);
}); });
......
...@@ -21,6 +21,22 @@ describe('Release detail mutations', () => { ...@@ -21,6 +21,22 @@ describe('Release detail mutations', () => {
release = convertObjectPropsToCamelCase(originalRelease); release = convertObjectPropsToCamelCase(originalRelease);
}); });
describe(`${types.INITIALIZE_EMPTY_RELEASE}`, () => {
it('set state.release to an empty release object', () => {
mutations[types.INITIALIZE_EMPTY_RELEASE](state);
expect(state.release).toEqual({
tagName: null,
name: '',
description: '',
milestones: [],
assets: {
links: [],
},
});
});
});
describe(`${types.REQUEST_RELEASE}`, () => { describe(`${types.REQUEST_RELEASE}`, () => {
it('set state.isFetchingRelease to true', () => { it('set state.isFetchingRelease to true', () => {
mutations[types.REQUEST_RELEASE](state); mutations[types.REQUEST_RELEASE](state);
...@@ -96,17 +112,17 @@ describe('Release detail mutations', () => { ...@@ -96,17 +112,17 @@ describe('Release detail mutations', () => {
}); });
}); });
describe(`${types.REQUEST_UPDATE_RELEASE}`, () => { describe(`${types.REQUEST_SAVE_RELEASE}`, () => {
it('set state.isUpdatingRelease to true', () => { it('set state.isUpdatingRelease to true', () => {
mutations[types.REQUEST_UPDATE_RELEASE](state); mutations[types.REQUEST_SAVE_RELEASE](state);
expect(state.isUpdatingRelease).toBe(true); expect(state.isUpdatingRelease).toBe(true);
}); });
}); });
describe(`${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => { describe(`${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => { it('handles a successful response from the server', () => {
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release); mutations[types.RECEIVE_SAVE_RELEASE_SUCCESS](state, release);
expect(state.updateError).toBeUndefined(); expect(state.updateError).toBeUndefined();
...@@ -114,10 +130,10 @@ describe('Release detail mutations', () => { ...@@ -114,10 +130,10 @@ describe('Release detail mutations', () => {
}); });
}); });
describe(`${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => { describe(`${types.RECEIVE_SAVE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => { it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' }; const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error); mutations[types.RECEIVE_SAVE_RELEASE_ERROR](state, error);
expect(state.isUpdatingRelease).toBe(false); expect(state.isUpdatingRelease).toBe(false);
......
import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
describe('releases/util.js', () => {
describe('releaseToApiJson', () => {
it('converts a release JavaScript object into JSON that the Release API can accept', () => {
const release = {
tagName: 'tag-name',
name: 'Release name',
description: 'Release description',
milestones: [{ id: 1, title: '13.2' }, { id: 2, title: '13.3' }],
assets: {
links: [{ url: 'https://gitlab.example.com/link', linkType: 'other' }],
},
};
const expectedJson = {
tag_name: 'tag-name',
ref: null,
name: 'Release name',
description: 'Release description',
milestones: ['13.2', '13.3'],
assets: {
links: [{ url: 'https://gitlab.example.com/link', link_type: 'other' }],
},
};
expect(releaseToApiJson(release)).toEqual(expectedJson);
});
describe('when createFrom is provided', () => {
it('adds the provided createFrom ref to the JSON as a "ref" property', () => {
const createFrom = 'main';
const release = {};
const expectedJson = {
ref: createFrom,
};
expect(releaseToApiJson(release, createFrom)).toMatchObject(expectedJson);
});
});
describe('when release.milestones is falsy', () => {
it('includes a "milestone" property in the returned result as an empty array', () => {
const release = {};
const expectedJson = {
milestones: [],
};
expect(releaseToApiJson(release)).toMatchObject(expectedJson);
});
});
});
describe('apiJsonToRelease', () => {
it('converts JSON received from the Release API into an object usable by the Vue application', () => {
const json = {
tag_name: 'tag-name',
assets: {
links: [
{
link_type: 'other',
},
],
},
};
const expectedRelease = {
tagName: 'tag-name',
assets: {
links: [
{
linkType: 'other',
},
],
},
milestones: [],
};
expect(apiJsonToRelease(json)).toEqual(expectedRelease);
});
});
});
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