Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/ide/components/file_templates/bar.vue80
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue125
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue47
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue8
-rw-r--r--app/assets/javascripts/ide/stores/actions.js3
-rw-r--r--app/assets/javascripts/ide/stores/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/actions.js24
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js7
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js17
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js2
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss14
12 files changed, 319 insertions, 14 deletions
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..23be5f45f16
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_templates/bar.vue
@@ -0,0 +1,80 @@
+<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: 'setInitialType',
+ },
+ mounted() {
+ this.setInitialType();
+ },
+ methods: {
+ ...mapActions('fileTemplates', [
+ 'setSelectedTemplateType',
+ 'fetchTemplate',
+ 'undoFileTemplate',
+ ]),
+ setInitialType() {
+ const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name);
+
+ if (initialTemplateType) {
+ this.setSelectedTemplateType(initialTemplateType);
+ }
+ },
+ selectTemplateType(templateType) {
+ this.setSelectedTemplateType(templateType);
+ },
+ selectTemplate(template) {
+ this.fetchTemplate(template);
+ },
+ undo() {
+ this.undoFileTemplate();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="d-flex align-items-center ide-file-templates">
+ <strong class="append-right-default">
+ {{ __('File templates') }}
+ </strong>
+ <dropdown
+ :data="templateTypes"
+ :label="selectedTemplateType.name || __('Choose a type...')"
+ class="mr-2"
+ @click="selectTemplateType"
+ />
+ <dropdown
+ v-if="showTemplatesDropdown"
+ :label="__('Choose a template...')"
+ :is-async-data="true"
+ :searchable="true"
+ :title="__('File templates')"
+ class="mr-2"
+ @click="selectTemplate"
+ />
+ <transition name="fade">
+ <button
+ v-show="updateSuccess"
+ type="button"
+ class="btn btn-default"
+ @click="undo"
+ >
+ {{ __('Undo') }}
+ </button>
+ </transition>
+ </div>
+</template>
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..13059937f85
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -0,0 +1,125 @@
+<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,
+ },
+ isAsyncData: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ searchable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ ...mapState('fileTemplates', ['templates', 'isLoading']),
+ outputData() {
+ return (this.isAsyncData ? this.templates : this.data).filter(t => {
+ if (!this.searchable) return true;
+
+ return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
+ });
+ },
+ showLoading() {
+ return this.isAsyncData ? 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.isAsyncData) {
+ this.fetchTemplateTypes();
+ }
+ },
+ clickItem(item) {
+ this.$emit('click', item);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown">
+ <dropdown-button
+ :toggle-text="label"
+ data-display="static"
+ />
+ <div class="dropdown-menu pb-0">
+ <div
+ v-if="title"
+ class="dropdown-title ml-0 mr-0"
+ >
+ {{ title }}
+ </div>
+ <div
+ v-if="!showLoading && searchable"
+ class="dropdown-input"
+ >
+ <input
+ v-model="search"
+ :placeholder="__('Filter...')"
+ type="search"
+ class="dropdown-input-field"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ ></i>
+ </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/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index e500ef0e1b5..bcd53ac1ba2 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,6 +1,7 @@
<script>
+import $ from 'jquery';
import { __ } from '~/locale';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants';
@@ -15,6 +16,7 @@ export default {
},
computed: {
...mapState(['entryModal']),
+ ...mapGetters('fileTemplates', ['templateTypes']),
entryName: {
get() {
if (this.entryModal.type === modalTypes.rename) {
@@ -31,7 +33,9 @@ export default {
if (this.entryModal.type === modalTypes.tree) {
return __('Create new directory');
} else if (this.entryModal.type === modalTypes.rename) {
- return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
+ return this.entryModal.entry.type === modalTypes.tree
+ ? __('Rename folder')
+ : __('Rename file');
}
return __('Create new file');
@@ -40,11 +44,16 @@ export default {
if (this.entryModal.type === modalTypes.tree) {
return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) {
- return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
+ return this.entryModal.entry.type === modalTypes.tree
+ ? __('Rename folder')
+ : __('Rename file');
}
return __('Create file');
},
+ isCreatingNew() {
+ return this.entryModal.type !== modalTypes.rename;
+ },
},
methods: {
...mapActions(['createTempEntry', 'renameEntry']),
@@ -61,6 +70,14 @@ export default {
});
}
},
+ createFromTemplate(template) {
+ this.createTempEntry({
+ name: template.name,
+ type: this.entryModal.type,
+ });
+
+ $('#ide-new-entry').modal('toggle');
+ },
focusInput() {
this.$refs.fieldName.focus();
},
@@ -77,6 +94,7 @@ export default {
:header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success"
+ modal-size="lg"
@submit="submitForm"
@open="focusInput"
@closed="closedModal"
@@ -84,16 +102,35 @@ export default {
<div
class="form-group row"
>
- <label class="label-bold col-form-label col-sm-3">
+ <label class="label-bold col-form-label col-sm-2">
{{ __('Name') }}
</label>
- <div class="col-sm-9">
+ <div class="col-sm-10">
<input
ref="fieldName"
v-model="entryName"
type="text"
class="form-control"
+ placeholder="/dir/file_name"
/>
+ <ul
+ v-if="isCreatingNew"
+ class="prepend-top-default list-inline"
+ >
+ <li
+ v-for="(template, index) in templateTypes"
+ :key="index"
+ class="list-inline-item"
+ >
+ <button
+ type="button"
+ class="btn btn-missing p-1 pr-2 pl-2"
+ @click="createFromTemplate(template)"
+ >
+ {{ template.name }}
+ </button>
+ </li>
+ </ul>
</div>
</div>
</gl-modal>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f55aa843444..d3a73e84cc7 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;
},
@@ -216,7 +219,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"
@@ -249,6 +252,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/actions.js b/app/assets/javascripts/ide/stores/actions.js
index aa02dfbddc4..aa6ca3f29cd 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -206,6 +206,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
const entry = state.entries[entryPath || path];
+
commit(types.RENAME_ENTRY, { path, name, entryPath });
if (entry.type === 'tree') {
@@ -214,7 +215,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath
);
}
- if (!entryPath) {
+ if (!entryPath && !entry.tempFile) {
dispatch('deleteEntry', path);
}
};
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index a601dc8f5a0..877d88bb060 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: 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
index 43237a29466..dd53213ed18 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
@@ -1,6 +1,7 @@
import Api from '~/api';
import { __ } from '~/locale';
import * as types from './mutation_types';
+import eventHub from '../../../eventhub';
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
export const receiveTemplateTypesError = ({ commit, dispatch }) => {
@@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => {
.catch(() => dispatch('receiveTemplateTypesError'));
};
-export const setSelectedTemplateType = ({ commit }, type) =>
+export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => {
commit(types.SET_SELECTED_TEMPLATE_TYPE, type);
+ if (rootGetters.activeFile.prevPath === type.name) {
+ dispatch('discardFileChanges', rootGetters.activeFile.path, { root: true });
+ } else if (rootGetters.activeFile.name !== type.name) {
+ dispatch(
+ 'renameEntry',
+ {
+ path: rootGetters.activeFile.path,
+ name: type.name,
+ },
+ { root: true },
+ );
+ }
+};
+
export const receiveTemplateError = ({ dispatch }, template) => {
dispatch(
'setErrorMessage',
@@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) =>
{ root: true },
);
commit(types.SET_UPDATE_SUCCESS, true);
+ eventHub.$emit(`editor.update.model.new.content.${rootGetters.activeFile.key}`, template.content);
};
export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
@@ -76,6 +92,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true });
commit(types.SET_UPDATE_SUCCESS, false);
+
+ eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.raw);
+
+ if (file.prevPath) {
+ dispatch('discardFileChanges', file.path, { root: true });
+ }
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index 38318fd49bf..628babe6a01 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,3 +1,5 @@
+import { activityBarViews } from '../../../constants';
+
export const templateTypes = () => [
{
name: '.gitlab-ci.yml',
@@ -17,7 +19,8 @@ export const templateTypes = () => [
},
];
-export const showFileTemplatesBar = (_, getters) => name =>
- getters.templateTypes.find(t => t.name === name);
+export const showFileTemplatesBar = (_, getters, rootState) => name =>
+ getters.templateTypes.find(t => t.name === name) &&
+ rootState.currentActivityView === activityBarViews.edit;
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
index dfa5ef54413..383ff5db392 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/index.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js
@@ -3,10 +3,10 @@ import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
-export default {
+export default () => ({
namespaced: true,
actions,
state: createState(),
getters,
mutations,
-};
+});
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index f2bb87ac674..2c8535bda59 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
@@ -226,7 +227,7 @@ export default {
path: newPath,
name: entryPath ? oldEntry.name : name,
tempFile: true,
- prevPath: oldEntry.path,
+ prevPath: oldEntry.tempFile ? null : oldEntry.path,
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
tree: [],
parentPath,
@@ -245,6 +246,20 @@ export default {
if (newEntry.type === 'blob') {
state.changedFiles = state.changedFiles.concat(newEntry);
}
+
+ if (state.entries[newPath].opened) {
+ state.openFiles.push(state.entries[newPath]);
+ }
+
+ if (oldEntry.tempFile) {
+ const filterMethod = f => f.path !== oldEntry.path;
+
+ state.openFiles = state.openFiles.filter(filterMethod);
+ state.changedFiles = state.changedFiles.filter(filterMethod);
+ parent.tree = parent.tree.filter(filterMethod);
+
+ Vue.delete(state.entries, oldEntry.path);
+ }
},
...projectMutations,
...mergeRequestMutation,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 66f29824898..6ca246c1d63 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -55,7 +55,7 @@ export default {
f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath),
);
- if (file.tempFile) {
+ if (file.tempFile && file.content === '') {
Object.assign(state.entries[file.path], {
content: raw,
});
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 5ff4e487d04..ce19ba4de07 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1442,3 +1442,17 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
top: 50%;
transform: translateY(-50%);
}
+
+.ide-file-templates {
+ padding: $grid-size $gl-padding;
+ background-color: $gray-light;
+ border-bottom: 1px solid $white-dark;
+
+ .dropdown {
+ min-width: 180px;
+ }
+
+ .dropdown-content {
+ max-height: 222px;
+ }
+}