Commit d75f02b2 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch 'nfriend-convert-release-new-edit-page-to-graphql' into 'master'

Convert New and Edit Release pages to use GraphQL

See merge request gitlab-org/gitlab!57000
parents 21b79e1a 51b2591b
mutation createRelease($input: ReleaseCreateInput!) {
releaseCreate(input: $input) {
release {
links {
selfUrl
}
}
errors
}
}
mutation createReleaseAssetLink($input: ReleaseAssetLinkCreateInput!) {
releaseAssetLinkCreate(input: $input) {
errors
}
}
mutation deleteReleaseAssetLink($input: ReleaseAssetLinkDeleteInput!) {
releaseAssetLinkDelete(input: $input) {
errors
}
}
mutation updateRelease($input: ReleaseUpdateInput!) {
releaseUpdate(input: $input) {
errors
}
}
......@@ -103,3 +103,39 @@ export const isValid = (_state, getters) => {
const errors = getters.validationErrors;
return Object.values(errors.assets.links).every(isEmpty) && !errors.isTagNameEmpty;
};
/** Returns all the variables for a `releaseUpdate` GraphQL mutation */
export const releaseUpdateMutatationVariables = (state) => {
const name = state.release.name?.trim().length > 0 ? state.release.name.trim() : null;
// Milestones may be either a list of milestone objects OR just a list
// of milestone titles. The GraphQL mutation requires only the titles be sent.
const milestones = (state.release.milestones || []).map((m) => m.title || m);
return {
input: {
projectPath: state.projectPath,
tagName: state.release.tagName,
name,
description: state.release.description,
milestones,
},
};
};
/** Returns all the variables for a `releaseCreate` GraphQL mutation */
export const releaseCreateMutatationVariables = (state, getters) => {
return {
input: {
...getters.releaseUpdateMutatationVariables.input,
ref: state.createFrom,
assets: {
links: getters.releaseLinksToCreate.map(({ name, url, linkType }) => ({
name,
url,
linkType: linkType.toUpperCase(),
})),
},
},
};
};
import { pick } from 'lodash';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
} from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
/**
* 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 name = release.name?.trim().length > 0 ? release.name.trim() : null;
// Milestones may be either a list of milestone objects OR just a list
// of milestone titles. The API requires only the titles be sent.
const milestones = (release.milestones || []).map((m) => m.title || m);
return convertObjectPropsToSnakeCase(
{
name,
tagName: release.tagName,
ref: createFrom,
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;
};
export const gqClient = createGqClient({}, { fetchPolicy: fetchPolicies.NO_CACHE });
const convertScalarProperties = (graphQLRelease) =>
......@@ -125,8 +82,7 @@ const convertMilestones = (graphQLRelease) => ({
/**
* Converts a single release object fetched from GraphQL
* into a release object that matches the shape of the REST API
* (the same shape that is returned by `apiJsonToRelease` above.)
* into a release object that matches the general structure of the REST API
*
* @param graphQLRelease The release object returned from a GraphQL query
*/
......
---
title: Speed up save on New/Edit Release page
merge_request: 57000
author:
type: performance
......@@ -26663,13 +26663,13 @@ msgstr ""
msgid "Releases|New Release"
msgstr ""
msgid "Release|Something went wrong while creating a new release"
msgid "Release|Something went wrong while creating a new release."
msgstr ""
msgid "Release|Something went wrong while getting the release details."
msgstr ""
msgid "Release|Something went wrong while saving the release details"
msgid "Release|Something went wrong while saving the release details."
msgstr ""
msgid "Remediations"
......
......@@ -257,4 +257,93 @@ describe('Release edit/new getters', () => {
});
});
});
describe.each([
[
'returns all the data needed for the releaseUpdate GraphQL query',
{
projectPath: 'projectPath',
release: {
tagName: 'release.tagName',
name: 'release.name',
description: 'release.description',
milestones: ['release.milestone[0].title'],
},
},
{
projectPath: 'projectPath',
tagName: 'release.tagName',
name: 'release.name',
description: 'release.description',
milestones: ['release.milestone[0].title'],
},
],
[
'trims whitespace from the release name',
{ release: { name: ' name \t\n' } },
{ name: 'name' },
],
[
'returns the name as null if the name is nothing but whitespace',
{ release: { name: ' \t\n' } },
{ name: null },
],
['returns the name as null if the name is undefined', { release: {} }, { name: null }],
[
'returns just the milestone titles even if the release includes full milestone objects',
{ release: { milestones: [{ title: 'release.milestone[0].title' }] } },
{ milestones: ['release.milestone[0].title'] },
],
])('releaseUpdateMutatationVariables', (description, state, expectedVariables) => {
it(description, () => {
const expectedVariablesObject = { input: expect.objectContaining(expectedVariables) };
const actualVariables = getters.releaseUpdateMutatationVariables(state);
expect(actualVariables).toEqual(expectedVariablesObject);
});
});
describe('releaseCreateMutatationVariables', () => {
it('returns all the data needed for the releaseCreate GraphQL query', () => {
const state = {
createFrom: 'main',
};
const otherGetters = {
releaseUpdateMutatationVariables: {
input: {
name: 'release.name',
},
},
releaseLinksToCreate: [
{
name: 'link.name',
url: 'link.url',
linkType: 'link.linkType',
},
],
};
const expectedVariables = {
input: {
name: 'release.name',
ref: 'main',
assets: {
links: [
{
name: 'link.name',
url: 'link.url',
linkType: 'LINK.LINKTYPE',
},
],
},
},
};
const actualVariables = getters.releaseCreateMutatationVariables(state, otherGetters);
expect(actualVariables).toEqual(expectedVariables);
});
});
});
import { cloneDeep } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures';
import {
releaseToApiJson,
apiJsonToRelease,
convertGraphQLRelease,
convertAllReleasesGraphQLResponse,
convertOneReleaseGraphQLResponse,
......@@ -19,106 +17,6 @@ const originalOneReleaseForEditingQueryResponse = getJSONFixture(
);
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: ['13.2', '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('release.name', () => {
it.each`
input | output
${null} | ${null}
${''} | ${null}
${' \t\n\r\n'} | ${null}
${' Release name '} | ${'Release name'}
`('converts a name like `$input` to `$output`', ({ input, output }) => {
const release = { name: input };
const expectedJson = {
name: output,
};
expect(releaseToApiJson(release)).toMatchObject(expectedJson);
});
});
describe('when milestones contains full milestone objects', () => {
it('converts the milestone objects into titles', () => {
const release = {
milestones: [{ title: '13.2' }, { title: '13.3' }, '13.4'],
};
const expectedJson = { milestones: ['13.2', '13.3', '13.4'] };
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);
});
});
describe('convertGraphQLRelease', () => {
let releaseFromResponse;
let convertedRelease;
......
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