Merge branch 'ide-file-templates' into 'master'
Added file templates to the Web IDE Closes #47947 See merge request gitlab-org/gitlab-ce!21245
This commit is contained in:
commit
2cffa02e39
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () => {};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added file templates to the Web IDE
|
||||
merge_request:
|
||||
author:
|
||||
type: added
|
|
@ -1170,6 +1170,12 @@ 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 template..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Choose a type..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Choose any color."
|
||||
msgstr ""
|
||||
|
||||
|
@ -2694,6 +2700,9 @@ msgstr ""
|
|||
msgid "Fields on this page are now uneditable, you can configure"
|
||||
msgstr ""
|
||||
|
||||
msgid "File templates"
|
||||
msgstr ""
|
||||
|
||||
msgid "Files"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2706,6 +2715,9 @@ msgstr ""
|
|||
msgid "Filter by commit message"
|
||||
msgstr ""
|
||||
|
||||
msgid "Filter..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Find by path"
|
||||
msgstr ""
|
||||
|
||||
|
@ -6199,6 +6211,9 @@ msgstr ""
|
|||
msgid "Unable to load the diff. %{button_try_again}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Undo"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unlock"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
import Vue from 'vue';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import Bar from '~/ide/components/file_templates/bar.vue';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { resetStore, file } from '../../helpers';
|
||||
|
||||
describe('IDE file templates bar component', () => {
|
||||
let Component;
|
||||
let vm;
|
||||
|
||||
beforeAll(() => {
|
||||
Component = Vue.extend(Bar);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const store = createStore();
|
||||
|
||||
store.state.openFiles.push({
|
||||
...file('file'),
|
||||
opened: true,
|
||||
active: true,
|
||||
});
|
||||
|
||||
vm = mountComponentWithStore(Component, { store });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
describe('template type dropdown', () => {
|
||||
it('renders dropdown component', () => {
|
||||
expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type');
|
||||
});
|
||||
|
||||
it('calls setSelectedTemplateType when clicking item', () => {
|
||||
spyOn(vm, 'setSelectedTemplateType').and.stub();
|
||||
|
||||
vm.$el.querySelector('.dropdown-content button').click();
|
||||
|
||||
expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
|
||||
name: '.gitlab-ci.yml',
|
||||
key: 'gitlab_ci_ymls',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('template dropdown', () => {
|
||||
beforeEach(done => {
|
||||
vm.$store.state.fileTemplates.templates = [
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
];
|
||||
vm.$store.state.fileTemplates.selectedTemplateType = {
|
||||
name: '.gitlab-ci.yml',
|
||||
key: 'gitlab_ci_ymls',
|
||||
};
|
||||
|
||||
vm.$nextTick(done);
|
||||
});
|
||||
|
||||
it('renders dropdown component', () => {
|
||||
expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template');
|
||||
});
|
||||
|
||||
it('calls fetchTemplate on click', () => {
|
||||
spyOn(vm, 'fetchTemplate').and.stub();
|
||||
|
||||
vm.$el
|
||||
.querySelectorAll('.dropdown-content')[1]
|
||||
.querySelector('button')
|
||||
.click();
|
||||
|
||||
expect(vm.fetchTemplate).toHaveBeenCalledWith({
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('shows undo button if updateSuccess is true', done => {
|
||||
vm.$store.state.fileTemplates.updateSuccess = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls undoFileTemplate when clicking undo button', () => {
|
||||
spyOn(vm, 'undoFileTemplate').and.stub();
|
||||
|
||||
vm.$el.querySelector('.btn-default').click();
|
||||
|
||||
expect(vm.undoFileTemplate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls setSelectedTemplateType if activeFile name matches a template', done => {
|
||||
const fileName = '.gitlab-ci.yml';
|
||||
|
||||
spyOn(vm, 'setSelectedTemplateType');
|
||||
vm.$store.state.openFiles[0].name = fileName;
|
||||
|
||||
vm.setInitialType();
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
|
||||
name: fileName,
|
||||
key: 'gitlab_ci_ymls',
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,201 @@
|
|||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import Dropdown from '~/ide/components/file_templates/dropdown.vue';
|
||||
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { resetStore } from '../../helpers';
|
||||
|
||||
describe('IDE file templates dropdown component', () => {
|
||||
let Component;
|
||||
let vm;
|
||||
|
||||
beforeAll(() => {
|
||||
Component = Vue.extend(Dropdown);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const store = createStore();
|
||||
|
||||
vm = createComponentWithStore(Component, store, {
|
||||
label: 'Test',
|
||||
}).$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
resetStore(vm.$store);
|
||||
});
|
||||
|
||||
describe('async', () => {
|
||||
beforeEach(() => {
|
||||
vm.isAsyncData = true;
|
||||
});
|
||||
|
||||
it('calls async store method on Bootstrap dropdown event', () => {
|
||||
spyOn(vm, 'fetchTemplateTypes').and.stub();
|
||||
|
||||
$(vm.$el).trigger('show.bs.dropdown');
|
||||
|
||||
expect(vm.fetchTemplateTypes).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders templates when async', done => {
|
||||
vm.$store.state.fileTemplates.templates = [
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
];
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders loading icon when isLoading is true', done => {
|
||||
vm.$store.state.fileTemplates.isLoading = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('searches template data', () => {
|
||||
vm.$store.state.fileTemplates.templates = [
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
];
|
||||
vm.searchable = true;
|
||||
vm.search = 'hello';
|
||||
|
||||
expect(vm.outputData).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not filter data is searchable is false', () => {
|
||||
vm.$store.state.fileTemplates.templates = [
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
];
|
||||
vm.search = 'hello';
|
||||
|
||||
expect(vm.outputData).toEqual([
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('calls clickItem on click', done => {
|
||||
spyOn(vm, 'clickItem').and.stub();
|
||||
|
||||
vm.$store.state.fileTemplates.templates = [
|
||||
{
|
||||
name: 'test',
|
||||
},
|
||||
];
|
||||
|
||||
vm.$nextTick(() => {
|
||||
vm.$el.querySelector('.dropdown-content button').click();
|
||||
|
||||
expect(vm.clickItem).toHaveBeenCalledWith({
|
||||
name: 'test',
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders input when searchable is true', done => {
|
||||
vm.searchable = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render input when searchable is true & showLoading is true', done => {
|
||||
vm.searchable = true;
|
||||
vm.$store.state.fileTemplates.isLoading = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.dropdown-input')).toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sync', () => {
|
||||
beforeEach(done => {
|
||||
vm.data = [
|
||||
{
|
||||
name: 'test sync',
|
||||
},
|
||||
];
|
||||
|
||||
vm.$nextTick(done);
|
||||
});
|
||||
|
||||
it('renders props data', () => {
|
||||
expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test sync');
|
||||
});
|
||||
|
||||
it('renders input when searchable is true', done => {
|
||||
vm.searchable = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls clickItem on click', done => {
|
||||
spyOn(vm, 'clickItem').and.stub();
|
||||
|
||||
vm.$nextTick(() => {
|
||||
vm.$el.querySelector('.dropdown-content button').click();
|
||||
|
||||
expect(vm.clickItem).toHaveBeenCalledWith({
|
||||
name: 'test sync',
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('searches template data', () => {
|
||||
vm.searchable = true;
|
||||
vm.search = 'hello';
|
||||
|
||||
expect(vm.outputData).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not filter data is searchable is false', () => {
|
||||
vm.search = 'hello';
|
||||
|
||||
expect(vm.outputData).toEqual([
|
||||
{
|
||||
name: 'test sync',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders dropdown title', done => {
|
||||
vm.title = 'Test title';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('Test title');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,6 +5,7 @@ import commitState from '~/ide/stores/modules/commit/state';
|
|||
import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
|
||||
import pipelinesState from '~/ide/stores/modules/pipelines/state';
|
||||
import branchesState from '~/ide/stores/modules/branches/state';
|
||||
import fileTemplatesState from '~/ide/stores/modules/file_templates/state';
|
||||
|
||||
export const resetStore = store => {
|
||||
const newState = {
|
||||
|
@ -13,6 +14,7 @@ export const resetStore = store => {
|
|||
mergeRequests: mergeRequestsState(),
|
||||
pipelines: pipelinesState(),
|
||||
branches: branchesState(),
|
||||
fileTemplates: fileTemplatesState(),
|
||||
};
|
||||
store.replaceState(newState);
|
||||
};
|
||||
|
|
|
@ -148,14 +148,66 @@ describe('IDE file templates actions', () => {
|
|||
});
|
||||
|
||||
describe('setSelectedTemplateType', () => {
|
||||
it('commits SET_SELECTED_TEMPLATE_TYPE', done => {
|
||||
testAction(
|
||||
actions.setSelectedTemplateType,
|
||||
'test',
|
||||
state,
|
||||
[{ type: types.SET_SELECTED_TEMPLATE_TYPE, payload: 'test' }],
|
||||
[],
|
||||
done,
|
||||
it('commits SET_SELECTED_TEMPLATE_TYPE', () => {
|
||||
const commit = jasmine.createSpy('commit');
|
||||
const options = {
|
||||
commit,
|
||||
dispatch() {},
|
||||
rootGetters: {
|
||||
activeFile: {
|
||||
name: 'test',
|
||||
prevPath: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
actions.setSelectedTemplateType(options, { name: 'test' });
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(types.SET_SELECTED_TEMPLATE_TYPE, { name: 'test' });
|
||||
});
|
||||
|
||||
it('dispatches discardFileChanges if prevPath matches templates name', () => {
|
||||
const dispatch = jasmine.createSpy('dispatch');
|
||||
const options = {
|
||||
commit() {},
|
||||
dispatch,
|
||||
rootGetters: {
|
||||
activeFile: {
|
||||
name: 'test',
|
||||
path: 'test',
|
||||
prevPath: 'test',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
actions.setSelectedTemplateType(options, { name: 'test' });
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true });
|
||||
});
|
||||
|
||||
it('dispatches renameEntry if file name doesnt match', () => {
|
||||
const dispatch = jasmine.createSpy('dispatch');
|
||||
const options = {
|
||||
commit() {},
|
||||
dispatch,
|
||||
rootGetters: {
|
||||
activeFile: {
|
||||
name: 'oldtest',
|
||||
path: 'oldtest',
|
||||
prevPath: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
actions.setSelectedTemplateType(options, { name: 'test' });
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
'renameEntry',
|
||||
{
|
||||
path: 'oldtest',
|
||||
name: 'test',
|
||||
},
|
||||
{ root: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -332,5 +384,20 @@ describe('IDE file templates actions', () => {
|
|||
|
||||
expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false);
|
||||
});
|
||||
|
||||
it('dispatches discardFileChanges if file has prevPath', () => {
|
||||
const dispatch = jasmine.createSpy('dispatch');
|
||||
const rootGetters = {
|
||||
activeFile: { path: 'test', prevPath: 'newtest', raw: 'raw content' },
|
||||
};
|
||||
|
||||
actions.undoFileTemplate({ dispatch, commit() {}, rootGetters });
|
||||
|
||||
expect(dispatch.calls.mostRecent().args).toEqual([
|
||||
'discardFileChanges',
|
||||
'test',
|
||||
{ root: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import createState from '~/ide/stores/state';
|
||||
import { activityBarViews } from '~/ide/constants';
|
||||
import * as getters from '~/ide/stores/modules/file_templates/getters';
|
||||
|
||||
describe('IDE file templates getters', () => {
|
||||
|
@ -8,22 +10,49 @@ describe('IDE file templates getters', () => {
|
|||
});
|
||||
|
||||
describe('showFileTemplatesBar', () => {
|
||||
it('finds template type by name', () => {
|
||||
let rootState;
|
||||
|
||||
beforeEach(() => {
|
||||
rootState = createState();
|
||||
});
|
||||
|
||||
it('returns true if template is found and currentActivityView is edit', () => {
|
||||
rootState.currentActivityView = activityBarViews.edit;
|
||||
|
||||
expect(
|
||||
getters.showFileTemplatesBar(null, {
|
||||
templateTypes: getters.templateTypes(),
|
||||
})('LICENSE'),
|
||||
).toEqual({
|
||||
name: 'LICENSE',
|
||||
key: 'licenses',
|
||||
});
|
||||
getters.showFileTemplatesBar(
|
||||
null,
|
||||
{
|
||||
templateTypes: getters.templateTypes(),
|
||||
},
|
||||
rootState,
|
||||
)('LICENSE'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if template is found and currentActivityView is not edit', () => {
|
||||
rootState.currentActivityView = activityBarViews.commit;
|
||||
|
||||
expect(
|
||||
getters.showFileTemplatesBar(
|
||||
null,
|
||||
{
|
||||
templateTypes: getters.templateTypes(),
|
||||
},
|
||||
rootState,
|
||||
)('LICENSE'),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns undefined if not found', () => {
|
||||
expect(
|
||||
getters.showFileTemplatesBar(null, {
|
||||
templateTypes: getters.templateTypes(),
|
||||
})('test'),
|
||||
getters.showFileTemplatesBar(
|
||||
null,
|
||||
{
|
||||
templateTypes: getters.templateTypes(),
|
||||
},
|
||||
rootState,
|
||||
)('test'),
|
||||
).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -339,5 +339,13 @@ describe('Multi-file store mutations', () => {
|
|||
|
||||
expect(localState.entries.parentPath.tree.length).toBe(1);
|
||||
});
|
||||
|
||||
it('adds to openFiles if previously opened', () => {
|
||||
localState.entries.oldPath.opened = true;
|
||||
|
||||
mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
|
||||
|
||||
expect(localState.openFiles).toEqual([localState.entries.newPath]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue