summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-08-16 12:41:21 +0100
committerPhil Hughes <me@iamphill.com>2018-08-16 12:41:21 +0100
commit9252c88d160255512e435f2d1badaab72eb5528a (patch)
tree5a52a227a23bf12349ade41c59d129cd5022de13
parenta2d647b1137821ef87fc177ac85ac775f2ce55c1 (diff)
downloadgitlab-ce-ide-file-templates.tar.gz
Added file templates to the Web IDEide-file-templates
Closes #47947
-rw-r--r--app/assets/javascripts/api.js7
-rw-r--r--app/assets/javascripts/ide/components/file_templates/bar.vue90
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue120
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue8
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/actions.js43
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js23
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/index.js12
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/mutations.js21
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/state.js6
-rw-r--r--changelogs/unreleased/ide-file-templates.yml5
-rw-r--r--locale/gitlab.pot15
13 files changed, 358 insertions, 1 deletions
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/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue
new file mode 100644
index 00000000000..8344b5440e9
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_templates/bar.vue
@@ -0,0 +1,90 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Dropdown from './dropdown.vue';
+
+export default {
+ components: {
+ Dropdown,
+ },
+ computed: {
+ ...mapGetters(['activeFile']),
+ ...mapGetters('fileTemplates', ['templateTypes']),
+ ...mapState('fileTemplates', ['selectedTemplateType', 'updateSuccess']),
+ showTemplatesDropdown() {
+ return Object.keys(this.selectedTemplateType).length > 0;
+ },
+ },
+ watch: {
+ activeFile: {
+ handler: 'setInitialType',
+ },
+ },
+ mounted() {
+ this.setInitialType();
+ },
+ methods: {
+ ...mapActions('fileTemplates', ['setTemplateType', 'fetchTemplate']),
+ setInitialType() {
+ const type = this.templateTypes.find(t => t.name === this.activeFile.name);
+
+ if (type) {
+ this.setTemplateType(type);
+ }
+ },
+ selectTemplateType(type) {
+ this.setTemplateType(type);
+ },
+ selecteTemplate(template) {
+ this.fetchTemplate(template);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex align-items-center ide-file-templates">
+ <strong class="mr-2">
+ {{ __('File templates') }}
+ </strong>
+ <dropdown
+ :data="templateTypes"
+ :label="selectedTemplateType.name || __('Choose a type...')"
+ class="mr-2"
+ @click="selectTemplateType"
+ />
+ <dropdown
+ v-if="showTemplatesDropdown"
+ :label="__('Choose a type...')"
+ :async="true"
+ :searchable="true"
+ :title="__('File templates')"
+ class="mr-2"
+ @click="selecteTemplate"
+ />
+ <transition name="fade">
+ <div v-show="updateSuccess">
+ <strong class="text-success mr-2">
+ {{ __('Template applied') }}
+ </strong>
+ <button
+ type="button"
+ class="btn btn-default"
+ >
+ {{ __('Undo') }}
+ </button>
+ </div>
+ </transition>
+ </div>
+</template>
+
+<style>
+.ide-file-templates {
+ padding: 8px 16px;
+ background-color: #fafafa;
+ border-bottom: 1px solid #eaeaea;
+}
+
+.ide-file-templates .dropdown {
+ min-width: 180px;
+}
+</style>
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
new file mode 100644
index 00000000000..914022d8764
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -0,0 +1,120 @@
+<script>
+import $ from 'jquery';
+import { mapActions, mapState } from 'vuex';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+export default {
+ components: {
+ DropdownButton,
+ LoadingIcon,
+ },
+ props: {
+ data: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ async: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ searchable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ ...mapState('fileTemplates', ['templates', 'isLoading']),
+ outputData() {
+ return (this.async ? this.templates : this.data).filter(t => {
+ if (!this.searchable) return true;
+
+ return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
+ });
+ },
+ showLoading() {
+ return this.async ? this.isLoading : false;
+ },
+ },
+ mounted() {
+ $(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync);
+ },
+ beforeDestroy() {
+ $(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync);
+ },
+ methods: {
+ ...mapActions('fileTemplates', ['fetchTemplateTypes']),
+ fetchTemplatesIfAsync() {
+ if (this.async) {
+ this.fetchTemplateTypes();
+ }
+ },
+ clickItem(item) {
+ this.$emit('click', item);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown">
+ <dropdown-button
+ :toggle-text="label"
+ />
+ <div class="dropdown-menu">
+ <div
+ v-if="title"
+ class="dropdown-title"
+ >
+ {{ title }}
+ </div>
+ <div
+ v-if="!showLoading && searchable"
+ class="dropdown-input"
+ >
+ <input
+ v-model="search"
+ :placeholder="__('Filter...')"
+ type="search"
+ class="dropdown-input-field"
+ />
+ </div>
+ <div class="dropdown-content">
+ <loading-icon
+ v-if="showLoading"
+ size="2"
+ />
+ <ul v-else>
+ <li
+ v-for="(item, index) in outputData"
+ :key="index"
+ >
+ <button
+ type="button"
+ @click="clickItem(item)"
+ >
+ {{ item.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f9badb01535..e657ed14a4c 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor';
import ExternalLink from './external_link.vue';
+import FileTemplatesBar from './file_templates/bar.vue';
export default {
components: {
ContentViewer,
DiffViewer,
ExternalLink,
+ FileTemplatesBar,
},
props: {
file: {
@@ -34,6 +36,7 @@ export default {
'isCommitModeActive',
'isReviewModeActive',
]),
+ ...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.content;
},
@@ -217,7 +220,7 @@ export default {
id="ide"
class="blob-viewer-container blob-editor-container"
>
- <div class="ide-mode-tabs clearfix" >
+ <div class="ide-mode-tabs clearfix">
<ul
v-if="!shouldHideEditor && isEditModeActive"
class="nav-links float-left"
@@ -250,6 +253,9 @@ export default {
:file="file"
/>
</div>
+ <file-templates-bar
+ v-if="showFileTemplatesBar(file.name)"
+ />
<div
v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor"
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index a601dc8f5a0..3af39ce62fd 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -8,6 +8,7 @@ import commitModule from './modules/commit';
import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests';
import branches from './modules/branches';
+import fileTemplates from './modules/file_templates';
Vue.use(Vuex);
@@ -22,6 +23,7 @@ export const createStore = () =>
pipelines,
mergeRequests,
branches,
+ fileTemplates,
},
});
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..31075ca5891
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
@@ -0,0 +1,43 @@
+import Api from '~/api';
+import * as types from './mutation_types';
+
+export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
+export const receiveTemplateTypesError = ({ commit }) => commit(types.RECEIVE_TEMPLATE_TYPES_ERROR);
+export const receiveTemplateTypesSuccess = ({ commit }, templates) =>
+ commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates);
+
+export const fetchTemplateTypes = ({ dispatch, state }) => {
+ if (state.selectedTemplateType === '') return Promise.reject();
+
+ dispatch('requestTemplateTypes');
+
+ return Api.templates(state.selectedTemplateType.key)
+ .then(({ data }) => dispatch('receiveTemplateTypesSuccess', data))
+ .catch(() => dispatch('receiveTemplateTypesSuccess'));
+};
+
+export const setTemplateType = ({ commit }, type) => commit(types.SET_TEMPLATE_TYPE, type);
+
+export const updateFile = ({ dispatch, commit, rootGetters }, template) => {
+ dispatch(
+ 'changeFileContent',
+ { path: rootGetters.activeFile.path, content: template.content },
+ { root: true },
+ );
+ commit(types.SET_UPDATE_SUCCESS, true);
+};
+
+export const fetchTemplate = ({ dispatch, state }, template) => {
+ if (template.content) {
+ dispatch('updateFile', template);
+ return Promise.resolve();
+ }
+
+ return Api.templates(`${state.selectedTemplateType.key}/${template.key || template.name}`).then(
+ ({ data }) => {
+ dispatch('updateFile', data);
+ },
+ );
+};
+
+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..b7e2fd948c3
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js
@@ -0,0 +1,12 @@
+import state from './state';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+
+export default {
+ namespaced: true,
+ actions,
+ state,
+ 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..8d31e50ef80
--- /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_TEMPLATE_TYPE = 'SET_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..808553491b4
--- /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_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..77196192bfa
--- /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/changelogs/unreleased/ide-file-templates.yml b/changelogs/unreleased/ide-file-templates.yml
new file mode 100644
index 00000000000..68983670b25
--- /dev/null
+++ b/changelogs/unreleased/ide-file-templates.yml
@@ -0,0 +1,5 @@
+---
+title: Added file templates to the Web IDE
+merge_request:
+author:
+type: added
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b01a0068694..e9889f46f4a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1113,6 +1113,9 @@ msgstr ""
msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
msgstr ""
+msgid "Choose a type..."
+msgstr ""
+
msgid "Choose any color."
msgstr ""
@@ -2559,6 +2562,9 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure"
msgstr ""
+msgid "File templates"
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -2571,6 +2577,9 @@ msgstr ""
msgid "Filter by commit message"
msgstr ""
+msgid "Filter..."
+msgstr ""
+
msgid "Find by path"
msgstr ""
@@ -5285,6 +5294,9 @@ msgstr ""
msgid "Template"
msgstr ""
+msgid "Template applied"
+msgstr ""
+
msgid "Terms of Service Agreement and Privacy Policy"
msgstr ""
@@ -5799,6 +5811,9 @@ msgstr ""
msgid "Unable to load the diff. %{button_try_again}"
msgstr ""
+msgid "Undo"
+msgstr ""
+
msgid "Unlock"
msgstr ""