From d80149e60e0bfe6d5ea58a6d25a56914f1f2848e Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 17 Aug 2018 09:29:59 +0100 Subject: [PATCH 1/3] Added store for file templates in the Web IDE #47947 --- app/assets/javascripts/api.js | 7 + .../stores/modules/file_templates/actions.js | 81 +++++ .../stores/modules/file_templates/getters.js | 23 ++ .../stores/modules/file_templates/index.js | 12 + .../modules/file_templates/mutation_types.js | 7 + .../modules/file_templates/mutations.js | 21 ++ .../stores/modules/file_templates/state.js | 6 + .../modules/file_templates/actions_spec.js | 336 ++++++++++++++++++ .../modules/file_templates/getters_spec.js | 30 ++ .../modules/file_templates/mutations_spec.js | 61 ++++ 10 files changed, 584 insertions(+) create mode 100644 app/assets/javascripts/ide/stores/modules/file_templates/actions.js create mode 100644 app/assets/javascripts/ide/stores/modules/file_templates/getters.js create mode 100644 app/assets/javascripts/ide/stores/modules/file_templates/index.js create mode 100644 app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js create mode 100644 app/assets/javascripts/ide/stores/modules/file_templates/mutations.js create mode 100644 app/assets/javascripts/ide/stores/modules/file_templates/state.js create mode 100644 spec/javascripts/ide/stores/modules/file_templates/actions_spec.js create mode 100644 spec/javascripts/ide/stores/modules/file_templates/getters_spec.js create mode 100644 spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 25fe2ae553e..cd800d75f7a 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -15,6 +15,7 @@ const Api = { mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', groupLabelsPath: '/groups/:namespace_path/-/labels', + templatesPath: '/api/:version/templates/:key', licensePath: '/api/:version/templates/licenses/:key', gitignorePath: '/api/:version/templates/gitignores/:key', gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', @@ -265,6 +266,12 @@ const Api = { }); }, + templates(key, params = {}) { + const url = Api.buildUrl(this.templatesPath).replace(':key', key); + + return axios.get(url, { params }); + }, + buildUrl(url) { let urlRoot = ''; if (gon.relative_url_root != null) { diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js new file mode 100644 index 00000000000..c86310be379 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -0,0 +1,81 @@ +import Api from '~/api'; +import { __ } from '~/locale'; +import * as types from './mutation_types'; + +export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES); +export const receiveTemplateTypesError = ({ commit, dispatch }) => { + commit(types.RECEIVE_TEMPLATE_TYPES_ERROR); + dispatch( + 'setErrorMessage', + { + text: __('Error loading template types.'), + action: () => + dispatch('fetchTemplateTypes').then(() => + dispatch('setErrorMessage', null, { root: true }), + ), + actionText: __('Please try again'), + }, + { root: true }, + ); +}; +export const receiveTemplateTypesSuccess = ({ commit }, templates) => + commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates); + +export const fetchTemplateTypes = ({ dispatch, state }) => { + if (!Object.keys(state.selectedTemplateType).length) return Promise.reject(); + + dispatch('requestTemplateTypes'); + + return Api.templates(state.selectedTemplateType.key) + .then(({ data }) => dispatch('receiveTemplateTypesSuccess', data)) + .catch(() => dispatch('receiveTemplateTypesError')); +}; + +export const setSelectedTemplateType = ({ commit }, type) => + commit(types.SET_SELECTED_TEMPLATE_TYPE, type); + +export const receiveTemplateError = ({ dispatch }, template) => { + dispatch( + 'setErrorMessage', + { + text: __('Error loading template.'), + action: payload => + dispatch('fetchTemplateTypes', payload).then(() => + dispatch('setErrorMessage', null, { root: true }), + ), + actionText: __('Please try again'), + actionPayload: template, + }, + { root: true }, + ); +}; + +export const fetchTemplate = ({ dispatch, state }, template) => { + if (template.content) { + return dispatch('setFileTemplate', template); + } + + return Api.templates(`${state.selectedTemplateType.key}/${template.key || template.name}`) + .then(({ data }) => { + dispatch('setFileTemplate', data); + }) + .catch(() => dispatch('receiveTemplateError', template)); +}; + +export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) => { + dispatch( + 'changeFileContent', + { path: rootGetters.activeFile.path, content: template.content }, + { root: true }, + ); + commit(types.SET_UPDATE_SUCCESS, true); +}; + +export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { + const file = rootGetters.activeFile; + + dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true }); + commit(types.SET_UPDATE_SUCCESS, false); +}; + +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js new file mode 100644 index 00000000000..38318fd49bf --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js @@ -0,0 +1,23 @@ +export const templateTypes = () => [ + { + name: '.gitlab-ci.yml', + key: 'gitlab_ci_ymls', + }, + { + name: '.gitignore', + key: 'gitignores', + }, + { + name: 'LICENSE', + key: 'licenses', + }, + { + name: 'Dockerfile', + key: 'dockerfiles', + }, +]; + +export const showFileTemplatesBar = (_, getters) => name => + getters.templateTypes.find(t => t.name === name); + +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/index.js b/app/assets/javascripts/ide/stores/modules/file_templates/index.js new file mode 100644 index 00000000000..dfa5ef54413 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js @@ -0,0 +1,12 @@ +import createState from './state'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +export default { + namespaced: true, + actions, + state: createState(), + getters, + mutations, +}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js new file mode 100644 index 00000000000..cf4499c0264 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js @@ -0,0 +1,7 @@ +export const REQUEST_TEMPLATE_TYPES = 'REQUEST_TEMPLATE_TYPES'; +export const RECEIVE_TEMPLATE_TYPES_ERROR = 'RECEIVE_TEMPLATE_TYPES_ERROR'; +export const RECEIVE_TEMPLATE_TYPES_SUCCESS = 'RECEIVE_TEMPLATE_TYPES_SUCCESS'; + +export const SET_SELECTED_TEMPLATE_TYPE = 'SET_SELECTED_TEMPLATE_TYPE'; + +export const SET_UPDATE_SUCCESS = 'SET_UPDATE_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js new file mode 100644 index 00000000000..e413e61eaaa --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js @@ -0,0 +1,21 @@ +/* eslint-disable no-param-reassign */ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_TEMPLATE_TYPES](state) { + state.isLoading = true; + }, + [types.RECEIVE_TEMPLATE_TYPES_ERROR](state) { + state.isLoading = false; + }, + [types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, templates) { + state.isLoading = false; + state.templates = templates; + }, + [types.SET_SELECTED_TEMPLATE_TYPE](state, type) { + state.selectedTemplateType = type; + }, + [types.SET_UPDATE_SUCCESS](state, success) { + state.updateSuccess = success; + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/state.js b/app/assets/javascripts/ide/stores/modules/file_templates/state.js new file mode 100644 index 00000000000..bd4b7d7bc52 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/file_templates/state.js @@ -0,0 +1,6 @@ +export default () => ({ + isLoading: false, + templates: [], + selectedTemplateType: {}, + updateSuccess: false, +}); diff --git a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js new file mode 100644 index 00000000000..f831a9f0a5d --- /dev/null +++ b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js @@ -0,0 +1,336 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import createState from '~/ide/stores/modules/file_templates/state'; +import * as actions from '~/ide/stores/modules/file_templates/actions'; +import * as types from '~/ide/stores/modules/file_templates/mutation_types'; +import testAction from 'spec/helpers/vuex_action_helper'; + +describe('IDE file templates actions', () => { + let state; + let mock; + + beforeEach(() => { + state = createState(); + + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('requestTemplateTypes', () => { + it('commits REQUEST_TEMPLATE_TYPES', done => { + testAction( + actions.requestTemplateTypes, + null, + state, + [{ type: types.REQUEST_TEMPLATE_TYPES }], + [], + done, + ); + }); + }); + + describe('receiveTemplateTypesError', () => { + it('commits RECEIVE_TEMPLATE_TYPES_ERROR and dispatches setErrorMessage', done => { + testAction( + actions.receiveTemplateTypesError, + null, + state, + [{ type: types.RECEIVE_TEMPLATE_TYPES_ERROR }], + [ + { + type: 'setErrorMessage', + payload: { + action: jasmine.any(Function), + actionText: 'Please try again', + text: 'Error loading template types.', + }, + }, + ], + done, + ); + }); + }); + + describe('receiveTemplateTypesSuccess', () => { + it('commits RECEIVE_TEMPLATE_TYPES_SUCCESS', done => { + testAction( + actions.receiveTemplateTypesSuccess, + 'test', + state, + [{ type: types.RECEIVE_TEMPLATE_TYPES_SUCCESS, payload: 'test' }], + [], + done, + ); + }); + }); + + describe('fetchTemplateTypes', () => { + describe('success', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(200, [ + { + name: 'MIT', + }, + ]); + }); + + it('rejects if selectedTemplateType is empty', done => { + const dispatch = jasmine.createSpy('dispatch'); + + actions + .fetchTemplateTypes({ dispatch, state }) + .then(done.fail) + .catch(() => { + expect(dispatch).not.toHaveBeenCalled(); + + done(); + }); + }); + + it('dispatches actions', done => { + state.selectedTemplateType = { + key: 'licenses', + }; + + testAction( + actions.fetchTemplateTypes, + null, + state, + [], + [ + { + type: 'requestTemplateTypes', + }, + { + type: 'receiveTemplateTypesSuccess', + payload: [ + { + name: 'MIT', + }, + ], + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(500); + }); + + it('dispatches actions', done => { + state.selectedTemplateType = { + key: 'licenses', + }; + + testAction( + actions.fetchTemplateTypes, + null, + state, + [], + [ + { + type: 'requestTemplateTypes', + }, + { + type: 'receiveTemplateTypesError', + }, + ], + done, + ); + }); + }); + }); + + describe('setSelectedTemplateType', () => { + it('commits SET_SELECTED_TEMPLATE_TYPE', done => { + testAction( + actions.setSelectedTemplateType, + 'test', + state, + [{ type: types.SET_SELECTED_TEMPLATE_TYPE, payload: 'test' }], + [], + done, + ); + }); + }); + + describe('receiveTemplateError', () => { + it('dispatches setErrorMessage', done => { + testAction( + actions.receiveTemplateError, + 'test', + state, + [], + [ + { + type: 'setErrorMessage', + payload: { + action: jasmine.any(Function), + actionText: 'Please try again', + text: 'Error loading template.', + actionPayload: 'test', + }, + }, + ], + done, + ); + }); + }); + + describe('fetchTemplate', () => { + describe('success', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/templates\/licenses\/mit/).replyOnce(200, { + content: 'MIT content', + }); + mock.onGet(/api\/(.*)\/templates\/licenses\/testing/).replyOnce(200, { + content: 'testing content', + }); + }); + + it('dispatches setFileTemplate if template already has content', done => { + const template = { + content: 'already has content', + }; + + testAction( + actions.fetchTemplate, + template, + state, + [], + [{ type: 'setFileTemplate', payload: template }], + done, + ); + }); + + it('dispatches success', done => { + const template = { + key: 'mit', + }; + + state.selectedTemplateType = { + key: 'licenses', + }; + + testAction( + actions.fetchTemplate, + template, + state, + [], + [{ type: 'setFileTemplate', payload: { content: 'MIT content' } }], + done, + ); + }); + + it('dispatches success and uses name key for API call', done => { + const template = { + name: 'testing', + }; + + state.selectedTemplateType = { + key: 'licenses', + }; + + testAction( + actions.fetchTemplate, + template, + state, + [], + [{ type: 'setFileTemplate', payload: { content: 'testing content' } }], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/templates\/licenses\/mit/).replyOnce(500); + }); + + it('dispatches error', done => { + const template = { + name: 'testing', + }; + + state.selectedTemplateType = { + key: 'licenses', + }; + + testAction( + actions.fetchTemplate, + template, + state, + [], + [{ type: 'receiveTemplateError', payload: template }], + done, + ); + }); + }); + }); + + describe('setFileTemplate', () => { + it('dispatches changeFileContent', () => { + const dispatch = jasmine.createSpy('dispatch'); + const commit = jasmine.createSpy('commit'); + const rootGetters = { + activeFile: { path: 'test' }, + }; + + actions.setFileTemplate({ dispatch, commit, rootGetters }, { content: 'content' }); + + expect(dispatch).toHaveBeenCalledWith( + 'changeFileContent', + { path: 'test', content: 'content' }, + { root: true }, + ); + }); + + it('commits SET_UPDATE_SUCCESS', () => { + const dispatch = jasmine.createSpy('dispatch'); + const commit = jasmine.createSpy('commit'); + const rootGetters = { + activeFile: { path: 'test' }, + }; + + actions.setFileTemplate({ dispatch, commit, rootGetters }, { content: 'content' }); + + expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', true); + }); + }); + + describe('undoFileTemplate', () => { + it('dispatches changeFileContent', () => { + const dispatch = jasmine.createSpy('dispatch'); + const commit = jasmine.createSpy('commit'); + const rootGetters = { + activeFile: { path: 'test', raw: 'raw content' }, + }; + + actions.undoFileTemplate({ dispatch, commit, rootGetters }); + + expect(dispatch).toHaveBeenCalledWith( + 'changeFileContent', + { path: 'test', content: 'raw content' }, + { root: true }, + ); + }); + + it('commits SET_UPDATE_SUCCESS', () => { + const dispatch = jasmine.createSpy('dispatch'); + const commit = jasmine.createSpy('commit'); + const rootGetters = { + activeFile: { path: 'test', raw: 'raw content' }, + }; + + actions.undoFileTemplate({ dispatch, commit, rootGetters }); + + expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js b/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js new file mode 100644 index 00000000000..e337c3f331b --- /dev/null +++ b/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js @@ -0,0 +1,30 @@ +import * as getters from '~/ide/stores/modules/file_templates/getters'; + +describe('IDE file templates getters', () => { + describe('templateTypes', () => { + it('returns list of template types', () => { + expect(getters.templateTypes().length).toBe(4); + }); + }); + + describe('showFileTemplatesBar', () => { + it('finds template type by name', () => { + expect( + getters.showFileTemplatesBar(null, { + templateTypes: getters.templateTypes(), + })('LICENSE'), + ).toEqual({ + name: 'LICENSE', + key: 'licenses', + }); + }); + + it('returns undefined if not found', () => { + expect( + getters.showFileTemplatesBar(null, { + templateTypes: getters.templateTypes(), + })('test'), + ).toBe(undefined); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js b/spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js new file mode 100644 index 00000000000..a51527d699f --- /dev/null +++ b/spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js @@ -0,0 +1,61 @@ +import createState from '~/ide/stores/modules/file_templates/state'; +import * as types from '~/ide/stores/modules/file_templates/mutation_types'; +import mutations from '~/ide/stores/modules/file_templates/mutations'; + +describe('IDE file templates mutations', () => { + let state; + + beforeEach(() => { + state = createState(); + }); + + describe(types.REQUEST_TEMPLATE_TYPES, () => { + it('sets isLoading', () => { + mutations[types.REQUEST_TEMPLATE_TYPES](state); + + expect(state.isLoading).toBe(true); + }); + }); + + describe(types.RECEIVE_TEMPLATE_TYPES_ERROR, () => { + it('sets isLoading', () => { + state.isLoading = true; + + mutations[types.RECEIVE_TEMPLATE_TYPES_ERROR](state); + + expect(state.isLoading).toBe(false); + }); + }); + + describe(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, () => { + it('sets isLoading to false', () => { + state.isLoading = true; + + mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, []); + + expect(state.isLoading).toBe(false); + }); + + it('sets templates', () => { + mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, ['test']); + + expect(state.templates).toEqual(['test']); + }); + }); + + describe(types.SET_SELECTED_TEMPLATE_TYPE, () => { + it('sets selectedTemplateType', () => { + mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, 'type'); + + expect(state.selectedTemplateType).toBe('type'); + }); + }); + + describe(types.SET_UPDATE_SUCCESS, () => { + it('sets updateSuccess', () => { + mutations[types.SET_UPDATE_SUCCESS](state, true); + + expect(state.updateSuccess).toBe(true); + }); + }); +}); From c5481bebd4b51d2a6e710498441de60efe7d2df3 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 17 Aug 2018 11:50:31 +0100 Subject: [PATCH 2/3] fixed static-analysis --- locale/gitlab.pot | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b370cc13f11..3497957d072 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2442,6 +2442,12 @@ msgstr "" msgid "Error loading project data. Please try again." msgstr "" +msgid "Error loading template types." +msgstr "" + +msgid "Error loading template." +msgstr "" + msgid "Error occurred when toggling the notification subscription" msgstr "" From bdf70248c9b6ae82ec145e1b01e47c2219d17038 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 21 Aug 2018 10:18:23 +0100 Subject: [PATCH 3/3] added rewire comment to action default export --- .../javascripts/ide/stores/modules/file_templates/actions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js index c86310be379..43237a29466 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -78,4 +78,5 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { commit(types.SET_UPDATE_SUCCESS, false); }; +// prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {};