diff --git a/ee/app/assets/javascripts/api.js b/ee/app/assets/javascripts/api.js new file mode 100644 index 0000000000000000000000000000000000000000..aebea5a7f4bf093d210235a828a649cf424bd88b --- /dev/null +++ b/ee/app/assets/javascripts/api.js @@ -0,0 +1,41 @@ +import CEApi from '~/api'; +import axios from '~/lib/utils/axios_utils'; + +export default { + ...CEApi, + projectApprovalRulesPath: '/api/:version/projects/:id/approval_rules', + projectApprovalRulePath: '/api/:version/projects/:id/approval_rules/:ruleid', + getProjectApprovalRules(projectId) { + const url = this.buildUrl(this.projectApprovalRulesPath).replace( + ':id', + encodeURIComponent(projectId), + ); + + return axios.get(url); + }, + + postProjectApprovalRule(projectId, rule) { + const url = this.buildUrl(this.projectApprovalRulesPath).replace( + ':id', + encodeURIComponent(projectId), + ); + + return axios.post(url, rule); + }, + + putProjectApprovalRule(projectId, ruleId, rule) { + const url = this.buildUrl(this.projectApprovalRulePath) + .replace(':id', encodeURIComponent(projectId)) + .replace(':ruleid', encodeURIComponent(ruleId)); + + return axios.put(url, rule); + }, + + deleteProjectApprovalRule(projectId, ruleId) { + const url = this.buildUrl(this.projectApprovalRulePath) + .replace(':id', encodeURIComponent(projectId)) + .replace(':ruleid', encodeURIComponent(ruleId)); + + return axios.delete(url); + }, +}; diff --git a/ee/app/assets/javascripts/approvals/mappers.js b/ee/app/assets/javascripts/approvals/mappers.js new file mode 100644 index 0000000000000000000000000000000000000000..4d67822cf7bbc84b563b37264edcb3befab63866 --- /dev/null +++ b/ee/app/assets/javascripts/approvals/mappers.js @@ -0,0 +1,19 @@ +export const mapApprovalRuleRequest = req => ({ + name: req.name, + approvals_required: req.approvalsRequired, + users: req.users, + groups: req.groups, +}); + +export const mapApprovalRuleResponse = res => ({ + id: res.id, + name: res.name, + approvalsRequired: res.approvals_required, + approvers: res.approvers, + users: res.users, + groups: res.groups, +}); + +export const mapApprovalRulesResponse = req => ({ + rules: req.rules.map(mapApprovalRuleResponse), +}); diff --git a/ee/app/assets/javascripts/approvals/services/approvals_service_stub.js b/ee/app/assets/javascripts/approvals/services/approvals_service_stub.js deleted file mode 100644 index 4288f87452066429cbfaf571917aa20ddb2248b1..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/approvals/services/approvals_service_stub.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This provides a stubbed API for approval rule requests. - * - * **PLEASE NOTE:** - * - This class will be removed when the BE is merged for https://gitlab.com/gitlab-org/gitlab-ee/issues/1979 - */ - -export function createApprovalsServiceStub() { - const projectApprovalRules = []; - - return { - getProjectApprovalRules() { - return Promise.resolve({ - data: { rules: projectApprovalRules }, - }); - }, - }; -} - -export default createApprovalsServiceStub(); diff --git a/ee/app/assets/javascripts/approvals/stores/actions.js b/ee/app/assets/javascripts/approvals/stores/actions.js index 1b121b508783b079e5a4014728e88b110c41ecbf..6a133a851c7bc9862c2f400e64336c88bca94d15 100644 --- a/ee/app/assets/javascripts/approvals/stores/actions.js +++ b/ee/app/assets/javascripts/approvals/stores/actions.js @@ -1,7 +1,8 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; +import Api from 'ee/api'; import * as types from './mutation_types'; -import service from '../services/approvals_service_stub'; +import { mapApprovalRuleRequest, mapApprovalRulesResponse } from '../mappers'; export const setSettings = ({ commit }, settings) => { commit(types.SET_SETTINGS, settings); @@ -22,13 +23,56 @@ export const receiveRulesError = () => { export const fetchRules = ({ state, dispatch }) => { if (state.isLoading) { - return; + return Promise.resolve(); } + const { projectId } = state.settings; + dispatch('requestRules'); - service - .getProjectApprovalRules() - .then(response => dispatch('receiveRulesSuccess', response.data)) + return Api.getProjectApprovalRules(projectId) + .then(response => dispatch('receiveRulesSuccess', mapApprovalRulesResponse(response.data))) .catch(() => dispatch('receiveRulesError')); }; + +export const postRuleSuccess = ({ dispatch }) => { + dispatch('createModal/close'); + dispatch('fetchRules'); +}; + +export const postRuleError = () => { + createFlash(__('An error occurred while updating approvers')); +}; + +export const postRule = ({ state, dispatch }, rule) => { + const { projectId } = state.settings; + + return Api.postProjectApprovalRule(projectId, mapApprovalRuleRequest(rule)) + .then(() => dispatch('postRuleSuccess')) + .catch(() => dispatch('postRuleError')); +}; + +export const putRule = ({ state, dispatch }, { id, ...newRule }) => { + const { projectId } = state.settings; + + return Api.putProjectApprovalRule(projectId, id, mapApprovalRuleRequest(newRule)) + .then(() => dispatch('postRuleSuccess')) + .catch(() => dispatch('postRuleError')); +}; + +export const deleteRuleSuccess = ({ dispatch }) => { + dispatch('deleteModal/close'); + dispatch('fetchRules'); +}; + +export const deleteRuleError = () => { + createFlash(__('An error occurred while deleting the approvers group')); +}; + +export const deleteRule = ({ state, dispatch }, id) => { + const { projectId } = state.settings; + + return Api.deleteProjectApprovalRule(projectId, id) + .then(() => dispatch('deleteRuleSuccess')) + .catch(() => dispatch('deleteRuleError')); +}; diff --git a/ee/app/assets/javascripts/approvals/stores/index.js b/ee/app/assets/javascripts/approvals/stores/index.js index 64c78d2cb4ab7d14f06702030d0565d1ade8c8ba..8d34f826efc3f7bf4c83cd8aafa5d4b43de6f993 100644 --- a/ee/app/assets/javascripts/approvals/stores/index.js +++ b/ee/app/assets/javascripts/approvals/stores/index.js @@ -1,4 +1,5 @@ import Vuex from 'vuex'; +import modalModule from '~/vuex_shared/modules/modal'; import state from './state'; import mutations from './mutations'; import * as actions from './actions'; @@ -8,4 +9,8 @@ export default () => state: state(), mutations, actions, + modules: { + createModal: modalModule(), + deleteModal: modalModule(), + }, }); diff --git a/ee/spec/javascripts/api_spec.js b/ee/spec/javascripts/api_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..a0674c2539157a4f682f9a1a19bc57da4fd0a13f --- /dev/null +++ b/ee/spec/javascripts/api_spec.js @@ -0,0 +1,90 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import Api from 'ee/api'; +import { TEST_HOST } from 'spec/test_constants'; + +const TEST_API_VERSION = 'v3000'; +const TEST_PROJECT_ID = 17; +const TEST_RULE_ID = 22; + +describe('EE Api', () => { + let originalGon; + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + originalGon = window.gon; + window.gon = { + api_version: TEST_API_VERSION, + relative_url_root: TEST_HOST, + }; + }); + + afterEach(() => { + mock.restore(); + window.gon = originalGon; + }); + + describe('getProjectApprovalRules', () => { + it('gets with projectApprovalRulesPath', done => { + const expectedData = { rules: [] }; + const expectedUrl = `${TEST_HOST}${Api.projectApprovalRulesPath}` + .replace(':version', TEST_API_VERSION) + .replace(':id', TEST_PROJECT_ID); + + mock.onGet(expectedUrl).reply(200, expectedData); + Api.getProjectApprovalRules(TEST_PROJECT_ID) + .then(response => { + expect(response.data).toEqual(expectedData); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('postProjectApprovalRule', () => { + it('posts with projectApprovalRulesPath', done => { + const expectedUrl = `${TEST_HOST}${Api.projectApprovalRulesPath}` + .replace(':version', TEST_API_VERSION) + .replace(':id', TEST_PROJECT_ID); + + mock.onPost(expectedUrl).reply(200); + Api.postProjectApprovalRule(TEST_PROJECT_ID) + .then(done) + .catch(done.fail); + }); + }); + + describe('putProjectApprovalRule', () => { + it('puts with projectApprovalRulePath', done => { + const rule = { name: 'Lorem' }; + const expectedUrl = `${TEST_HOST}${Api.projectApprovalRulePath}` + .replace(':version', TEST_API_VERSION) + .replace(':id', TEST_PROJECT_ID) + .replace(':ruleid', TEST_RULE_ID); + + mock.onPut(expectedUrl).reply(200); + Api.putProjectApprovalRule(TEST_PROJECT_ID, TEST_RULE_ID, rule) + .then(() => { + expect(mock.history.put.length).toBe(1); + expect(mock.history.put[0].data).toBe(JSON.stringify(rule)); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('deleteProjectApprovalRule', () => { + it('deletes with projectApprovalRulePath', done => { + const expectedUrl = `${TEST_HOST}${Api.projectApprovalRulePath}` + .replace(':version', TEST_API_VERSION) + .replace(':id', TEST_PROJECT_ID) + .replace(':ruleid', TEST_RULE_ID); + + mock.onDelete(expectedUrl).reply(200); + Api.deleteProjectApprovalRule(TEST_PROJECT_ID, TEST_RULE_ID) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/ee/spec/javascripts/approvals/stores/actions_spec.js b/ee/spec/javascripts/approvals/stores/actions_spec.js index 05ee18d8d757944c83c256eee6ad49f2ddf9883b..5a493f2db3632d915d1e08000b0c3de2c1e1fe7b 100644 --- a/ee/spec/javascripts/approvals/stores/actions_spec.js +++ b/ee/spec/javascripts/approvals/stores/actions_spec.js @@ -1,14 +1,39 @@ +import Api from 'ee/api'; import testAction from 'spec/helpers/vuex_action_helper'; import * as types from 'ee/approvals/stores/mutation_types'; import actionsModule, * as actions from 'ee/approvals/stores/actions'; -import service from 'ee/approvals/services/approvals_service_stub'; +import { mapApprovalRuleRequest, mapApprovalRulesResponse } from 'ee/approvals/mappers'; + +const TEST_PROJECT_ID = 9; +const TEST_RULE_ID = 7; +const TEST_RULE_REQUEST = { + name: 'Lorem', + approvalsRequired: 1, + groups: [7], + users: [8, 9], +}; +const TEST_RULE_RESPONSE = { + id: 7, + name: 'Ipsum', + approvals_required: 2, + approvers: [{ id: 7 }, { id: 8 }, { id: 9 }], + groups: [{ id: 4 }], + users: [{ id: 7 }, { id: 8 }], +}; describe('EE approvals store actions', () => { + let state; let flashSpy; beforeEach(() => { + state = { + settings: { projectId: TEST_PROJECT_ID }, + }; flashSpy = spyOnDependency(actionsModule, 'createFlash'); - spyOn(service, 'getProjectApprovalRules'); + spyOn(Api, 'getProjectApprovalRules'); + spyOn(Api, 'postProjectApprovalRule'); + spyOn(Api, 'putProjectApprovalRule'); + spyOn(Api, 'deleteProjectApprovalRule'); }); describe('setSettings', () => { @@ -72,31 +97,185 @@ describe('EE approvals store actions', () => { it('dispatches request/receive', done => { const response = { - data: { rules: [] }, + data: { rules: [TEST_RULE_RESPONSE] }, }; - service.getProjectApprovalRules.and.returnValue(Promise.resolve(response)); + Api.getProjectApprovalRules.and.returnValue(Promise.resolve(response)); testAction( actions.fetchRules, null, - {}, + state, [], - [{ type: 'requestRules' }, { type: 'receiveRulesSuccess', payload: response.data }], - done, + [ + { type: 'requestRules' }, + { type: 'receiveRulesSuccess', payload: mapApprovalRulesResponse(response.data) }, + ], + () => { + expect(Api.getProjectApprovalRules).toHaveBeenCalledWith(TEST_PROJECT_ID); + + done(); + }, ); }); it('dispatches request/receive on error', done => { - service.getProjectApprovalRules.and.returnValue(Promise.reject()); + Api.getProjectApprovalRules.and.returnValue(Promise.reject()); testAction( actions.fetchRules, null, - {}, + state, [], [{ type: 'requestRules' }, { type: 'receiveRulesError' }], done, ); }); }); + + describe('postRuleSuccess', () => { + it('closes modal and fetches', done => { + testAction( + actions.postRuleSuccess, + null, + {}, + [], + [{ type: 'createModal/close' }, { type: 'fetchRules' }], + done, + ); + }); + }); + + describe('postRuleError', () => { + it('creates a flash', () => { + expect(flashSpy).not.toHaveBeenCalled(); + + actions.postRuleError(); + + expect(flashSpy.calls.allArgs()).toEqual([[jasmine.stringMatching('error occurred')]]); + }); + }); + + describe('postRule', () => { + it('dispatches success on success', done => { + Api.postProjectApprovalRule.and.returnValue(Promise.resolve()); + + testAction( + actions.postRule, + TEST_RULE_REQUEST, + state, + [], + [{ type: 'postRuleSuccess' }], + () => { + expect(Api.postProjectApprovalRule).toHaveBeenCalledWith( + TEST_PROJECT_ID, + mapApprovalRuleRequest(TEST_RULE_REQUEST), + ); + done(); + }, + ); + }); + + it('dispatches error on error', done => { + Api.postProjectApprovalRule.and.returnValue(Promise.reject()); + + testAction( + actions.postRule, + TEST_RULE_REQUEST, + state, + [], + [{ type: 'postRuleError' }], + () => { + expect(Api.postProjectApprovalRule).toHaveBeenCalledWith( + TEST_PROJECT_ID, + mapApprovalRuleRequest(TEST_RULE_REQUEST), + ); + + done(); + }, + ); + }); + }); + + describe('putRule', () => { + it('dispatches success on success', done => { + Api.putProjectApprovalRule.and.returnValue(Promise.resolve()); + + testAction( + actions.putRule, + { id: TEST_RULE_ID, ...TEST_RULE_REQUEST }, + state, + [], + [{ type: 'postRuleSuccess' }], + () => { + expect(Api.putProjectApprovalRule).toHaveBeenCalledWith( + TEST_PROJECT_ID, + TEST_RULE_ID, + mapApprovalRuleRequest(TEST_RULE_REQUEST), + ); + done(); + }, + ); + }); + + it('dispatches error on error', done => { + Api.putProjectApprovalRule.and.returnValue(Promise.reject()); + + testAction( + actions.putRule, + { id: TEST_RULE_ID, ...TEST_RULE_REQUEST }, + state, + [], + [{ type: 'postRuleError' }], + done, + ); + }); + }); + + describe('deleteRuleSuccess', () => { + it('closes modal and fetches', done => { + testAction( + actions.deleteRuleSuccess, + null, + {}, + [], + [{ type: 'deleteModal/close' }, { type: 'fetchRules' }], + done, + ); + }); + }); + + describe('deleteRuleError', () => { + it('creates a flash', () => { + expect(flashSpy).not.toHaveBeenCalled(); + + actions.deleteRuleError(); + + expect(flashSpy.calls.allArgs()).toEqual([[jasmine.stringMatching('error occurred')]]); + }); + }); + + describe('deleteRule', () => { + it('dispatches success on success', done => { + Api.deleteProjectApprovalRule.and.returnValue(Promise.resolve()); + + testAction( + actions.deleteRule, + TEST_RULE_ID, + state, + [], + [{ type: 'deleteRuleSuccess' }], + () => { + expect(Api.deleteProjectApprovalRule).toHaveBeenCalledWith(TEST_PROJECT_ID, TEST_RULE_ID); + + done(); + }, + ); + }); + + it('dispatches error on error', done => { + Api.deleteProjectApprovalRule.and.returnValue(Promise.reject()); + + testAction(actions.deleteRule, TEST_RULE_ID, state, [], [{ type: 'deleteRuleError' }], done); + }); + }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7c97844e9024d545f6e4897b0074e8f2036ffac5..7067f7a24a61fe9516ce0995cecc83bfb0c710c5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -686,6 +686,9 @@ msgstr "" msgid "An error occurred while adding approver" msgstr "" +msgid "An error occurred while deleting the approvers group" +msgstr "" + msgid "An error occurred while deleting the comment" msgstr "" @@ -779,6 +782,9 @@ msgstr "" msgid "An error occurred while unsubscribing to notifications." msgstr "" +msgid "An error occurred while updating approvers" +msgstr "" + msgid "An error occurred while updating the comment" msgstr ""