Commit 37576eaa authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '212561-saving-changes-rest-service' into 'master'

Save changes in Static Site Editor using REST GitLab API

See merge request gitlab-org/gitlab!29286
parents a516e498 69a73762
......@@ -188,6 +188,15 @@ const Api = {
return axios.get(url, { params });
},
createProjectMergeRequest(projectPath, options) {
const url = Api.buildUrl(Api.projectMergeRequestsPath).replace(
':id',
encodeURIComponent(projectPath),
);
return axios.post(url, options);
},
// Return Merge Request for project
projectMergeRequest(projectPath, mergeRequestId, params = {}) {
const url = Api.buildUrl(Api.projectMergeRequestPath)
......
import { s__ } from '~/locale';
export const BRANCH_SUFFIX_COUNT = 8;
export const DEFAULT_TARGET_BRANCH = 'master';
export const SUBMIT_CHANGES_BRANCH_ERROR = s__('StaticSiteEditor|Branch could not be created.');
export const SUBMIT_CHANGES_COMMIT_ERROR = s__(
'StaticSiteEditor|Could not commit the content changes.',
);
export const SUBMIT_CHANGES_MERGE_REQUEST_ERROR = s__(
'StaticSiteEditor|Could not create merge request.',
);
import { BRANCH_SUFFIX_COUNT, DEFAULT_TARGET_BRANCH } from '../constants';
const generateBranchSuffix = () => `${Date.now()}`.substr(BRANCH_SUFFIX_COUNT);
const generateBranchName = (username, targetBranch = DEFAULT_TARGET_BRANCH) =>
`${username}-${targetBranch}-patch-${generateBranchSuffix()}`;
export default generateBranchName;
// TODO implement
const submitContentChanges = () => new Promise(resolve => setTimeout(resolve, 1000));
import Api from '~/api';
import { s__, sprintf } from '~/locale';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
import {
DEFAULT_TARGET_BRANCH,
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
} from '../constants';
const createBranch = (projectId, branch) =>
Api.createBranch(projectId, {
ref: DEFAULT_TARGET_BRANCH,
branch,
}).catch(() => {
throw new Error(SUBMIT_CHANGES_BRANCH_ERROR);
});
const commitContent = (projectId, message, branch, sourcePath, content) =>
Api.commitMultiple(
projectId,
convertObjectPropsToSnakeCase({
branch,
commitMessage: message,
actions: [
convertObjectPropsToSnakeCase({
action: 'update',
filePath: sourcePath,
content,
}),
],
}),
).catch(() => {
throw new Error(SUBMIT_CHANGES_COMMIT_ERROR);
});
const createMergeRequest = (projectId, title, sourceBranch, targetBranch = DEFAULT_TARGET_BRANCH) =>
Api.createProjectMergeRequest(
projectId,
convertObjectPropsToSnakeCase({
title,
sourceBranch,
targetBranch,
}),
).catch(() => {
throw new Error(SUBMIT_CHANGES_MERGE_REQUEST_ERROR);
});
const submitContentChanges = ({ username, projectId, sourcePath, content }) => {
const branch = generateBranchName(username);
const mergeRequestTitle = sprintf(s__(`StaticSiteEditor|Update %{sourcePath} file`), {
sourcePath,
});
const meta = {};
return createBranch(projectId, branch)
.then(() => {
Object.assign(meta, { branch: { label: branch } });
return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content);
})
.then(({ data: { short_id: label, web_url: url } }) => {
Object.assign(meta, { commit: { label, url } });
return createMergeRequest(projectId, mergeRequestTitle, branch);
})
.then(({ data: { iid: label, web_url: url } }) => {
Object.assign(meta, { mergeRequest: { label, url } });
return meta;
});
};
export default submitContentChanges;
......@@ -10,6 +10,8 @@ const createState = (initialState = {}) => ({
content: '',
title: '',
savedContentMeta: null,
...initialState,
});
......
---
title: Save changes in Static Site Editor using REST GitLab API
merge_request: 29286
author:
type: added
......@@ -19368,6 +19368,15 @@ msgstr ""
msgid "Static Application Security Testing (SAST)"
msgstr ""
msgid "StaticSiteEditor|Branch could not be created."
msgstr ""
msgid "StaticSiteEditor|Could not commit the content changes."
msgstr ""
msgid "StaticSiteEditor|Could not create merge request."
msgstr ""
msgid "StaticSiteEditor|Return to site"
msgstr ""
......@@ -19377,6 +19386,9 @@ msgstr ""
msgid "StaticSiteEditor|Summary of changes"
msgstr ""
msgid "StaticSiteEditor|Update %{sourcePath} file"
msgstr ""
msgid "StaticSiteEditor|View merge request"
msgstr ""
......
......@@ -651,7 +651,7 @@ describe('Api', () => {
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
mock.onDelete(expectedUrl).replyOnce(500);
mock.onPost(expectedUrl).replyOnce(500);
return Api.getRawFile(dummyProjectPath, dummyFilePath).catch(() => {
expect(mock.history.get).toHaveLength(1);
......@@ -659,4 +659,36 @@ describe('Api', () => {
});
});
});
describe('createProjectMergeRequest', () => {
const dummyProjectPath = 'gitlab-org/gitlab';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
dummyProjectPath,
)}/merge_requests`;
const options = {
source_branch: 'feature',
target_branch: 'master',
title: 'Add feature',
};
describe('when the merge request is successfully created', () => {
it('resolves the Promise', () => {
mock.onPost(expectedUrl, options).replyOnce(201);
return Api.createProjectMergeRequest(dummyProjectPath, options).then(() => {
expect(mock.history.post).toHaveLength(1);
});
});
});
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
mock.onPost(expectedUrl).replyOnce(500);
return Api.createProjectMergeRequest(dummyProjectPath).catch(() => {
expect(mock.history.post).toHaveLength(1);
});
});
});
});
});
......@@ -34,3 +34,11 @@ export const savedContentMeta = {
};
export const submitChangesError = 'Could not save changes';
export const commitMultipleResponse = {
short_id: 'ed899a2f4b5',
web_url: '/commit/ed899a2f4b5',
};
export const createMergeRequestResponse = {
iid: '123',
web_url: '/merge_requests/123',
};
import { DEFAULT_TARGET_BRANCH, BRANCH_SUFFIX_COUNT } from '~/static_site_editor/constants';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
import { username } from '../mock_data';
describe('generateBranchName', () => {
const timestamp = 12345678901234;
beforeEach(() => {
jest.spyOn(Date, 'now').mockReturnValueOnce(timestamp);
});
it('generates a name that includes the username and target branch', () => {
expect(generateBranchName(username)).toMatch(`${username}-${DEFAULT_TARGET_BRANCH}`);
});
it(`adds the first ${BRANCH_SUFFIX_COUNT} numbers of the current timestamp`, () => {
expect(generateBranchName(username)).toMatch(
timestamp.toString().substring(BRANCH_SUFFIX_COUNT),
);
});
});
import Api from '~/api';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
import {
DEFAULT_TARGET_BRANCH,
SUBMIT_CHANGES_BRANCH_ERROR,
SUBMIT_CHANGES_COMMIT_ERROR,
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
} from '~/static_site_editor/constants';
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
import {
username,
projectId,
commitMultipleResponse,
createMergeRequestResponse,
sourcePath,
sourceContent as content,
} from '../mock_data';
jest.mock('~/static_site_editor/services/generate_branch_name');
describe('submitContentChanges', () => {
const mergeRequestTitle = `Update ${sourcePath} file`;
const branch = 'branch-name';
beforeEach(() => {
jest.spyOn(Api, 'createBranch').mockResolvedValue();
jest.spyOn(Api, 'commitMultiple').mockResolvedValue({ data: commitMultipleResponse });
jest
.spyOn(Api, 'createProjectMergeRequest')
.mockResolvedValue({ data: createMergeRequestResponse });
generateBranchName.mockReturnValue(branch);
});
it('creates a branch named after the username and target branch', () => {
return submitContentChanges({ username, projectId }).then(() => {
expect(Api.createBranch).toHaveBeenCalledWith(projectId, {
ref: DEFAULT_TARGET_BRANCH,
branch,
});
});
});
it('notifies error when branch could not be created', () => {
Api.createBranch.mockRejectedValueOnce();
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_BRANCH_ERROR,
);
});
it('commits the content changes to the branch when creating branch succeeds', () => {
return submitContentChanges({ username, projectId, sourcePath, content }).then(() => {
expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, {
branch,
commit_message: mergeRequestTitle,
actions: [
{
action: 'update',
file_path: sourcePath,
content,
},
],
});
});
});
it('notifies error when content could not be committed', () => {
Api.commitMultiple.mockRejectedValueOnce();
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_COMMIT_ERROR,
);
});
it('creates a merge request when commiting changes succeeds', () => {
return submitContentChanges({ username, projectId, sourcePath, content }).then(() => {
expect(Api.createProjectMergeRequest).toHaveBeenCalledWith(
projectId,
convertObjectPropsToSnakeCase({
title: mergeRequestTitle,
targetBranch: DEFAULT_TARGET_BRANCH,
sourceBranch: branch,
}),
);
});
});
it('notifies error when merge request could not be created', () => {
Api.createProjectMergeRequest.mockRejectedValueOnce();
expect(submitContentChanges({ username, projectId })).rejects.toThrow(
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
);
});
describe('when changes are submitted successfully', () => {
let result;
beforeEach(() => {
return submitContentChanges({ username, projectId, sourcePath, content }).then(_result => {
result = _result;
});
});
it('returns the branch name', () => {
expect(result).toMatchObject({ branch: { label: branch } });
});
it('returns commit short id and web url', () => {
expect(result).toMatchObject({
commit: {
label: commitMultipleResponse.short_id,
url: commitMultipleResponse.web_url,
},
});
});
it('returns merge request iid and web url', () => {
expect(result).toMatchObject({
mergeRequest: {
label: createMergeRequestResponse.iid,
url: createMergeRequestResponse.web_url,
},
});
});
});
});
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