Commit 2cffa02e authored by Tim Zallmann's avatar Tim Zallmann

Merge branch 'ide-file-templates' into 'master'

Added file templates to the Web IDE

Closes #47947

See merge request gitlab-org/gitlab-ce!21245
parents 63620ec7 21ed2da0
<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>
<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>
<script> <script>
import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import GlModal from '~/vue_shared/components/gl_modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { modalTypes } from '../../constants'; import { modalTypes } from '../../constants';
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
}, },
computed: { computed: {
...mapState(['entryModal']), ...mapState(['entryModal']),
...mapGetters('fileTemplates', ['templateTypes']),
entryName: { entryName: {
get() { get() {
if (this.entryModal.type === modalTypes.rename) { if (this.entryModal.type === modalTypes.rename) {
...@@ -31,7 +33,9 @@ export default { ...@@ -31,7 +33,9 @@ export default {
if (this.entryModal.type === modalTypes.tree) { if (this.entryModal.type === modalTypes.tree) {
return __('Create new directory'); return __('Create new directory');
} else if (this.entryModal.type === modalTypes.rename) { } 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'); return __('Create new file');
...@@ -40,11 +44,16 @@ export default { ...@@ -40,11 +44,16 @@ export default {
if (this.entryModal.type === modalTypes.tree) { if (this.entryModal.type === modalTypes.tree) {
return __('Create directory'); return __('Create directory');
} else if (this.entryModal.type === modalTypes.rename) { } 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'); return __('Create file');
}, },
isCreatingNew() {
return this.entryModal.type !== modalTypes.rename;
},
}, },
methods: { methods: {
...mapActions(['createTempEntry', 'renameEntry']), ...mapActions(['createTempEntry', 'renameEntry']),
...@@ -61,6 +70,14 @@ export default { ...@@ -61,6 +70,14 @@ export default {
}); });
} }
}, },
createFromTemplate(template) {
this.createTempEntry({
name: template.name,
type: this.entryModal.type,
});
$('#ide-new-entry').modal('toggle');
},
focusInput() { focusInput() {
this.$refs.fieldName.focus(); this.$refs.fieldName.focus();
}, },
...@@ -77,6 +94,7 @@ export default { ...@@ -77,6 +94,7 @@ export default {
:header-title-text="modalTitle" :header-title-text="modalTitle"
:footer-primary-button-text="buttonLabel" :footer-primary-button-text="buttonLabel"
footer-primary-button-variant="success" footer-primary-button-variant="success"
modal-size="lg"
@submit="submitForm" @submit="submitForm"
@open="focusInput" @open="focusInput"
@closed="closedModal" @closed="closedModal"
...@@ -84,16 +102,35 @@ export default { ...@@ -84,16 +102,35 @@ export default {
<div <div
class="form-group row" 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') }} {{ __('Name') }}
</label> </label>
<div class="col-sm-9"> <div class="col-sm-10">
<input <input
ref="fieldName" ref="fieldName"
v-model="entryName" v-model="entryName"
type="text" type="text"
class="form-control" 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>
</div> </div>
</gl-modal> </gl-modal>
......
...@@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; ...@@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { activityBarViews, viewerTypes } from '../constants'; import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import ExternalLink from './external_link.vue'; import ExternalLink from './external_link.vue';
import FileTemplatesBar from './file_templates/bar.vue';
export default { export default {
components: { components: {
ContentViewer, ContentViewer,
DiffViewer, DiffViewer,
ExternalLink, ExternalLink,
FileTemplatesBar,
}, },
props: { props: {
file: { file: {
...@@ -34,6 +36,7 @@ export default { ...@@ -34,6 +36,7 @@ export default {
'isCommitModeActive', 'isCommitModeActive',
'isReviewModeActive', 'isReviewModeActive',
]), ]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.content; return this.file && this.file.binary && !this.file.content;
}, },
...@@ -216,7 +219,7 @@ export default { ...@@ -216,7 +219,7 @@ export default {
id="ide" id="ide"
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div class="ide-mode-tabs clearfix" > <div class="ide-mode-tabs clearfix">
<ul <ul
v-if="!shouldHideEditor && isEditModeActive" v-if="!shouldHideEditor && isEditModeActive"
class="nav-links float-left" class="nav-links float-left"
...@@ -249,6 +252,9 @@ export default { ...@@ -249,6 +252,9 @@ export default {
:file="file" :file="file"
/> />
</div> </div>
<file-templates-bar
v-if="showFileTemplatesBar(file.name)"
/>
<div <div
v-show="!shouldHideEditor && file.viewMode ==='editor'" v-show="!shouldHideEditor && file.viewMode ==='editor'"
ref="editor" ref="editor"
......
...@@ -206,6 +206,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); ...@@ -206,6 +206,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => { export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
const entry = state.entries[entryPath || path]; const entry = state.entries[entryPath || path];
commit(types.RENAME_ENTRY, { path, name, entryPath }); commit(types.RENAME_ENTRY, { path, name, entryPath });
if (entry.type === 'tree') { if (entry.type === 'tree') {
...@@ -214,7 +215,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath ...@@ -214,7 +215,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath
); );
} }
if (!entryPath) { if (!entryPath && !entry.tempFile) {
dispatch('deleteEntry', path); dispatch('deleteEntry', path);
} }
}; };
......
...@@ -8,6 +8,7 @@ import commitModule from './modules/commit'; ...@@ -8,6 +8,7 @@ import commitModule from './modules/commit';
import pipelines from './modules/pipelines'; import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests'; import mergeRequests from './modules/merge_requests';
import branches from './modules/branches'; import branches from './modules/branches';
import fileTemplates from './modules/file_templates';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -22,6 +23,7 @@ export const createStore = () => ...@@ -22,6 +23,7 @@ export const createStore = () =>
pipelines, pipelines,
mergeRequests, mergeRequests,
branches, branches,
fileTemplates: fileTemplates(),
}, },
}); });
......
import Api from '~/api'; import Api from '~/api';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
import eventHub from '../../../eventhub';
export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES); export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
export const receiveTemplateTypesError = ({ commit, dispatch }) => { export const receiveTemplateTypesError = ({ commit, dispatch }) => {
...@@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => { ...@@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => {
.catch(() => dispatch('receiveTemplateTypesError')); .catch(() => dispatch('receiveTemplateTypesError'));
}; };
export const setSelectedTemplateType = ({ commit }, type) => export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => {
commit(types.SET_SELECTED_TEMPLATE_TYPE, 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) => { export const receiveTemplateError = ({ dispatch }, template) => {
dispatch( dispatch(
'setErrorMessage', 'setErrorMessage',
...@@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) => ...@@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) =>
{ root: true }, { root: true },
); );
commit(types.SET_UPDATE_SUCCESS, 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 }) => { export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
...@@ -76,6 +92,12 @@ 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 }); dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true });
commit(types.SET_UPDATE_SUCCESS, false); 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 // prevent babel-plugin-rewire from generating an invalid default during karma tests
......
import { activityBarViews } from '../../../constants';
export const templateTypes = () => [ export const templateTypes = () => [
{ {
name: '.gitlab-ci.yml', name: '.gitlab-ci.yml',
...@@ -17,7 +19,8 @@ export const templateTypes = () => [ ...@@ -17,7 +19,8 @@ export const templateTypes = () => [
}, },
]; ];
export const showFileTemplatesBar = (_, getters) => name => export const showFileTemplatesBar = (_, getters, rootState) => name =>
getters.templateTypes.find(t => t.name === name); getters.templateTypes.find(t => t.name === name) &&
rootState.currentActivityView === activityBarViews.edit;
export default () => {}; export default () => {};
...@@ -3,10 +3,10 @@ import * as actions from './actions'; ...@@ -3,10 +3,10 @@ import * as actions from './actions';
import * as getters from './getters'; import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
export default { export default () => ({
namespaced: true, namespaced: true,
actions, actions,
state: createState(), state: createState(),
getters, getters,
mutations, mutations,
}; });
import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project'; import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request'; import mergeRequestMutation from './mutations/merge_request';
...@@ -226,7 +227,7 @@ export default { ...@@ -226,7 +227,7 @@ export default {
path: newPath, path: newPath,
name: entryPath ? oldEntry.name : name, name: entryPath ? oldEntry.name : name,
tempFile: true, tempFile: true,
prevPath: oldEntry.path, prevPath: oldEntry.tempFile ? null : oldEntry.path,
url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath), url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
tree: [], tree: [],
parentPath, parentPath,
...@@ -245,6 +246,20 @@ export default { ...@@ -245,6 +246,20 @@ export default {
if (newEntry.type === 'blob') { if (newEntry.type === 'blob') {
state.changedFiles = state.changedFiles.concat(newEntry); 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, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
......
...@@ -55,7 +55,7 @@ export default { ...@@ -55,7 +55,7 @@ export default {
f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath), 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], { Object.assign(state.entries[file.path], {
content: raw, content: raw,
}); });
......
...@@ -1442,3 +1442,17 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -1442,3 +1442,17 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
top: 50%; top: 50%;
transform: translateY(-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;
}
}
---
title: Added file templates to the Web IDE
merge_request:
author:
type: added
...@@ -1170,6 +1170,12 @@ msgstr "" ...@@ -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." 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 "" msgstr ""
msgid "Choose a template..."
msgstr ""
msgid "Choose a type..."
msgstr ""
msgid "Choose any color." msgid "Choose any color."
msgstr "" msgstr ""
...@@ -2694,6 +2700,9 @@ msgstr "" ...@@ -2694,6 +2700,9 @@ msgstr ""
msgid "Fields on this page are now uneditable, you can configure" msgid "Fields on this page are now uneditable, you can configure"
msgstr "" msgstr ""
msgid "File templates"
msgstr ""
msgid "Files" msgid "Files"
msgstr "" msgstr ""
...@@ -2706,6 +2715,9 @@ msgstr "" ...@@ -2706,6 +2715,9 @@ msgstr ""
msgid "Filter by commit message" msgid "Filter by commit message"
msgstr "" msgstr ""
msgid "Filter..."
msgstr ""
msgid "Find by path" msgid "Find by path"
msgstr "" msgstr ""
...@@ -6199,6 +6211,9 @@ msgstr "" ...@@ -6199,6 +6211,9 @@ msgstr ""
msgid "Unable to load the diff. %{button_try_again}" msgid "Unable to load the diff. %{button_try_again}"
msgstr "" msgstr ""
msgid "Undo"
msgstr ""
msgid "Unlock" msgid "Unlock"
msgstr "" msgstr ""
......
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();
});
});
});
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'; ...@@ -5,6 +5,7 @@ import commitState from '~/ide/stores/modules/commit/state';
import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
import pipelinesState from '~/ide/stores/modules/pipelines/state'; import pipelinesState from '~/ide/stores/modules/pipelines/state';
import branchesState from '~/ide/stores/modules/branches/state'; import branchesState from '~/ide/stores/modules/branches/state';
import fileTemplatesState from '~/ide/stores/modules/file_templates/state';
export const resetStore = store => { export const resetStore = store => {
const newState = { const newState = {
...@@ -13,6 +14,7 @@ export const resetStore = store => { ...@@ -13,6 +14,7 @@ export const resetStore = store => {
mergeRequests: mergeRequestsState(), mergeRequests: mergeRequestsState(),
pipelines: pipelinesState(), pipelines: pipelinesState(),
branches: branchesState(), branches: branchesState(),
fileTemplates: fileTemplatesState(),
}; };
store.replaceState(newState); store.replaceState(newState);
}; };
......
...@@ -148,14 +148,66 @@ describe('IDE file templates actions', () => { ...@@ -148,14 +148,66 @@ describe('IDE file templates actions', () => {
}); });
describe('setSelectedTemplateType', () => { describe('setSelectedTemplateType', () => {
it('commits SET_SELECTED_TEMPLATE_TYPE', done => { it('commits SET_SELECTED_TEMPLATE_TYPE', () => {
testAction( const commit = jasmine.createSpy('commit');
actions.setSelectedTemplateType, const options = {
'test', commit,
state, dispatch() {},
[{ type: types.SET_SELECTED_TEMPLATE_TYPE, payload: 'test' }], rootGetters: {
[], activeFile: {
done, 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', () => { ...@@ -332,5 +384,20 @@ describe('IDE file templates actions', () => {
expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false); 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 },
]);
});
}); });
}); });
import createState from '~/ide/stores/state';
import { activityBarViews } from '~/ide/constants';
import * as getters from '~/ide/stores/modules/file_templates/getters'; import * as getters from '~/ide/stores/modules/file_templates/getters';
describe('IDE file templates getters', () => { describe('IDE file templates getters', () => {
...@@ -8,22 +10,49 @@ describe('IDE file templates getters', () => { ...@@ -8,22 +10,49 @@ describe('IDE file templates getters', () => {
}); });
describe('showFileTemplatesBar', () => { 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( expect(
getters.showFileTemplatesBar(null, { getters.showFileTemplatesBar(
null,
{
templateTypes: getters.templateTypes(), templateTypes: getters.templateTypes(),
})('LICENSE'), },
).toEqual({ rootState,
name: 'LICENSE', )('LICENSE'),
key: 'licenses', ).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', () => { it('returns undefined if not found', () => {
expect( expect(
getters.showFileTemplatesBar(null, { getters.showFileTemplatesBar(
null,
{
templateTypes: getters.templateTypes(), templateTypes: getters.templateTypes(),
})('test'), },
rootState,
)('test'),
).toBe(undefined); ).toBe(undefined);
}); });
}); });
......
...@@ -339,5 +339,13 @@ describe('Multi-file store mutations', () => { ...@@ -339,5 +339,13 @@ describe('Multi-file store mutations', () => {
expect(localState.entries.parentPath.tree.length).toBe(1); 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]);
});
}); });
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment