Commit d017d2d9 authored by Fatih Acet's avatar Fatih Acet

Merge branch '45687-web-ide-empty-state' into 'master'

Resolve "WebIDE doesn't work on empty repositories"

Closes #60107 and #45687

See merge request gitlab-org/gitlab-ce!26556
parents 4ca791e6 0d7afb95
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import FindFile from '~/vue_shared/components/file_finder/index.vue'; import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue'; import NewModal from './new_dropdown/modal.vue';
...@@ -22,6 +23,8 @@ export default { ...@@ -22,6 +23,8 @@ export default {
FindFile, FindFile,
ErrorMessage, ErrorMessage,
CommitEditorHeader, CommitEditorHeader,
GlButton,
GlLoadingIcon,
}, },
props: { props: {
rightPaneComponent: { rightPaneComponent: {
...@@ -47,13 +50,15 @@ export default { ...@@ -47,13 +50,15 @@ export default {
'someUncommittedChanges', 'someUncommittedChanges',
'isCommitModeActive', 'isCommitModeActive',
'allBlobs', 'allBlobs',
'emptyRepo',
'currentTree',
]), ]),
}, },
mounted() { mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e); window.onbeforeunload = e => this.onBeforeUnload(e);
}, },
methods: { methods: {
...mapActions(['toggleFileFinder']), ...mapActions(['toggleFileFinder', 'openNewEntryModal']),
onBeforeUnload(e = {}) { onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?'); const returnValue = __('Are you sure you want to lose unsaved changes?');
...@@ -98,17 +103,40 @@ export default { ...@@ -98,17 +103,40 @@ export default {
<repo-editor :file="activeFile" class="multi-file-edit-pane-content" /> <repo-editor :file="activeFile" class="multi-file-edit-pane-content" />
</template> </template>
<template v-else> <template v-else>
<div v-once class="ide-empty-state"> <div class="ide-empty-state">
<div class="row js-empty-state"> <div class="row js-empty-state">
<div class="col-12"> <div class="col-12">
<div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div> <div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div>
</div> </div>
<div class="col-12"> <div class="col-12">
<div class="text-content text-center"> <div class="text-content text-center">
<h4>Welcome to the GitLab IDE</h4> <h4>
<p> {{ __('Make and review changes in the browser with the Web IDE') }}
Select a file from the left sidebar to begin editing. Afterwards, you'll be able </h4>
to commit your changes. <template v-if="emptyRepo">
<p>
{{
__(
"Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes.",
)
}}
</p>
<gl-button
variant="success"
:title="__('New file')"
:aria-label="__('New file')"
@click="openNewEntryModal({ type: 'blob' })"
>
{{ __('New file') }}
</gl-button>
</template>
<gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="md" />
<p v-else>
{{
__(
"Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.",
)
}}
</p> </p>
</div> </div>
</div> </div>
......
...@@ -54,14 +54,17 @@ export default { ...@@ -54,14 +54,17 @@ export default {
<slot name="header"></slot> <slot name="header"></slot>
</header> </header>
<div class="ide-tree-body h-100"> <div class="ide-tree-body h-100">
<file-row <template v-if="currentTree.tree.length">
v-for="file in currentTree.tree" <file-row
:key="file.key" v-for="file in currentTree.tree"
:file="file" :key="file.key"
:level="0" :file="file"
:extra-component="$options.FileRowExtra" :level="0"
@toggleTreeOpen="toggleTreeOpen" :extra-component="$options.FileRowExtra"
/> @toggleTreeOpen="toggleTreeOpen"
/>
</template>
<div v-else class="file-row">{{ __('No files') }}</div>
</div> </div>
</template> </template>
</div> </div>
......
import $ from 'jquery'; import $ from 'jquery';
import Vue from 'vue'; import Vue from 'vue';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash'; import flash from '~/flash';
import _ from 'underscore';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { decorateFiles } from '../lib/files'; import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants'; import { stageKeys } from '../constants';
import service from '../services';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (self, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
...@@ -239,6 +242,53 @@ export const renameEntry = ( ...@@ -239,6 +242,53 @@ export const renameEntry = (
} }
}; };
export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
new Promise((resolve, reject) => {
const currentProject = state.projects[projectId];
if (!currentProject || !currentProject.branches[branchId] || force) {
service
.getBranchData(projectId, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, {
projectPath: projectId,
branchName: branchId,
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(e => {
if (e.response.status === 404) {
reject(e);
} else {
flash(
__('Error loading branch data. Please try again.'),
'alert',
document,
null,
false,
true,
);
reject(
new Error(
sprintf(
__('Branch not loaded - %{branchId}'),
{
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
},
false,
),
),
);
}
});
} else {
resolve(currentProject.branches[branchId]);
}
});
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -35,48 +35,6 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force ...@@ -35,48 +35,6 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
} }
}); });
export const getBranchData = (
{ commit, dispatch, state },
{ projectId, branchId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (
typeof state.projects[`${projectId}`] === 'undefined' ||
!state.projects[`${projectId}`].branches[branchId] ||
force
) {
service
.getBranchData(`${projectId}`, branchId)
.then(({ data }) => {
const { id } = data.commit;
commit(types.SET_BRANCH, {
projectPath: `${projectId}`,
branchName: branchId,
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
resolve(data);
})
.catch(e => {
if (e.response.status === 404) {
dispatch('showBranchNotFoundError', branchId);
} else {
flash(
__('Error loading branch data. Please try again.'),
'alert',
document,
null,
false,
true,
);
}
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) => export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service service
.getBranchData(projectId, branchId) .getBranchData(projectId, branchId)
...@@ -125,40 +83,66 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => { ...@@ -125,40 +83,66 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
}); });
}; };
export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => { export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
dispatch('setCurrentBranchId', branchId); const treePath = `${projectId}/${branchId}`;
commit(types.CREATE_TREE, { treePath });
dispatch('getBranchData', { commit(types.TOGGLE_LOADING, {
projectId, entry: state.trees[treePath],
branchId, forceValue: false,
}); });
};
return dispatch('getFiles', { export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
dispatch('setCurrentBranchId', branchId);
if (getters.emptyRepo) {
return dispatch('showEmptyState', { projectId, branchId });
}
return dispatch('getBranchData', {
projectId, projectId,
branchId, branchId,
}) })
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
} else {
dispatch('createTempEntry', {
name: path,
type: 'blob',
});
}
}
})
.then(() => { .then(() => {
dispatch('getMergeRequestsForBranch', { dispatch('getMergeRequestsForBranch', {
projectId, projectId,
branchId, branchId,
}); });
dispatch('getFiles', {
projectId,
branchId,
})
.then(() => {
if (basePath) {
const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
const treeEntryKey = Object.keys(state.entries).find(
key => key === path && !state.entries[key].pending,
);
const treeEntry = state.entries[treeEntryKey];
if (treeEntry) {
dispatch('handleTreeEntryAction', treeEntry);
} else {
dispatch('createTempEntry', {
name: path,
type: 'blob',
});
}
}
})
.catch(
() =>
new Error(
sprintf(
__('An error occurred whilst getting files for - %{branchId}'),
{
branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
},
false,
),
),
);
})
.catch(() => {
dispatch('showBranchNotFoundError', branchId);
}); });
}; };
...@@ -74,17 +74,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = ...@@ -74,17 +74,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
resolve(); resolve();
}) })
.catch(e => { .catch(e => {
if (e.response.status === 404) { dispatch('setErrorMessage', {
dispatch('showBranchNotFoundError', branchId); text: __('An error occurred whilst loading all the files.'),
} else { action: payload =>
dispatch('setErrorMessage', { dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
text: __('An error occurred whilst loading all the files.'), actionText: __('Please try again'),
action: payload => actionPayload: { projectId, branchId },
dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)), });
actionText: __('Please try again'),
actionPayload: { projectId, branchId },
});
}
reject(e); reject(e);
}); });
} else { } else {
......
...@@ -36,6 +36,9 @@ export const currentMergeRequest = state => { ...@@ -36,6 +36,9 @@ export const currentMergeRequest = state => {
export const currentProject = state => state.projects[state.currentProjectId]; export const currentProject = state => state.projects[state.currentProjectId];
export const emptyRepo = state =>
state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo;
export const currentTree = state => export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
......
...@@ -135,6 +135,17 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -135,6 +135,17 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return null; return null;
} }
if (!data.parent_ids.length) {
commit(
rootTypes.TOGGLE_EMPTY_STATE,
{
projectPath: rootState.currentProjectId,
value: false,
},
{ root: true },
);
}
dispatch('setLastCommitMessage', data); dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', ''); dispatch('updateCommitMessage', '');
return dispatch('updateFilesAfterCommit', { return dispatch('updateFilesAfterCommit', {
......
...@@ -12,6 +12,7 @@ export const SET_LINKS = 'SET_LINKS'; ...@@ -12,6 +12,7 @@ export const SET_LINKS = 'SET_LINKS';
export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN'; export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge Request Mutation Types // Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST'; export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
......
...@@ -19,6 +19,12 @@ export default { ...@@ -19,6 +19,12 @@ export default {
}); });
}, },
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) { [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
if (!state.projects[projectId].branches[branchId]) {
Object.assign(state.projects[projectId].branches, {
[branchId]: {},
});
}
Object.assign(state.projects[projectId].branches[branchId], { Object.assign(state.projects[projectId].branches[branchId], {
workingReference: reference, workingReference: reference,
}); });
......
...@@ -21,4 +21,9 @@ export default { ...@@ -21,4 +21,9 @@ export default {
}), }),
}); });
}, },
[types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) {
Object.assign(state.projects[projectPath], {
empty_repo: value,
});
},
}; };
---
title: Empty project state for Web IDE
merge_request: 26556
author:
type: added
...@@ -239,6 +239,7 @@ module API ...@@ -239,6 +239,7 @@ module API
end end
end end
expose :empty_repo?, as: :empty_repo
expose :archived?, as: :archived expose :archived?, as: :archived
expose :visibility expose :visibility
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group } expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
......
...@@ -952,6 +952,9 @@ msgstr "" ...@@ -952,6 +952,9 @@ msgstr ""
msgid "An error occurred whilst fetching the latest pipeline." msgid "An error occurred whilst fetching the latest pipeline."
msgstr "" msgstr ""
msgid "An error occurred whilst getting files for - %{branchId}"
msgstr ""
msgid "An error occurred whilst loading all the files." msgid "An error occurred whilst loading all the files."
msgstr "" msgstr ""
...@@ -1482,6 +1485,9 @@ msgstr "" ...@@ -1482,6 +1485,9 @@ msgstr ""
msgid "Branch name" msgid "Branch name"
msgstr "" msgstr ""
msgid "Branch not loaded - %{branchId}"
msgstr ""
msgid "BranchSwitcherPlaceholder|Search branches" msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "" msgstr ""
...@@ -2993,6 +2999,9 @@ msgstr "" ...@@ -2993,6 +2999,9 @@ msgstr ""
msgid "Create a new branch" msgid "Create a new branch"
msgstr "" msgstr ""
msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
msgstr ""
msgid "Create a new issue" msgid "Create a new issue"
msgstr "" msgstr ""
...@@ -5796,6 +5805,9 @@ msgstr "" ...@@ -5796,6 +5805,9 @@ msgstr ""
msgid "MRDiff|Show full file" msgid "MRDiff|Show full file"
msgstr "" msgstr ""
msgid "Make and review changes in the browser with the Web IDE"
msgstr ""
msgid "Make issue confidential." msgid "Make issue confidential."
msgstr "" msgstr ""
...@@ -6452,6 +6464,9 @@ msgstr "" ...@@ -6452,6 +6464,9 @@ msgstr ""
msgid "No file selected" msgid "No file selected"
msgstr "" msgstr ""
msgid "No files"
msgstr ""
msgid "No files found." msgid "No files found."
msgstr "" msgstr ""
...@@ -8615,6 +8630,9 @@ msgstr "" ...@@ -8615,6 +8630,9 @@ msgstr ""
msgid "Select Archive Format" msgid "Select Archive Format"
msgstr "" msgstr ""
msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes."
msgstr ""
msgid "Select a group to invite" msgid "Select a group to invite"
msgstr "" msgstr ""
......
...@@ -37,4 +37,39 @@ describe('Multi-file store branch mutations', () => { ...@@ -37,4 +37,39 @@ describe('Multi-file store branch mutations', () => {
expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit'); expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
}); });
}); });
describe('SET_BRANCH_WORKING_REFERENCE', () => {
beforeEach(() => {
localState.projects = {
Foo: {
branches: {
bar: {},
},
},
};
});
it('sets workingReference for existing branch', () => {
mutations.SET_BRANCH_WORKING_REFERENCE(localState, {
projectId: 'Foo',
branchId: 'bar',
reference: 'foo-bar-ref',
});
expect(localState.projects.Foo.branches.bar.workingReference).toBe('foo-bar-ref');
});
it('does not fail on non-existent just yet branch', () => {
expect(localState.projects.Foo.branches.unknown).toBeUndefined();
mutations.SET_BRANCH_WORKING_REFERENCE(localState, {
projectId: 'Foo',
branchId: 'unknown',
reference: 'fun-fun-ref',
});
expect(localState.projects.Foo.branches.unknown).not.toBeUndefined();
expect(localState.projects.Foo.branches.unknown.workingReference).toBe('fun-fun-ref');
});
});
}); });
import mutations from '~/ide/stores/mutations/project';
import state from '~/ide/stores/state';
describe('Multi-file store branch mutations', () => {
let localState;
beforeEach(() => {
localState = state();
localState.projects = { abcproject: { empty_repo: true } };
});
describe('TOGGLE_EMPTY_STATE', () => {
it('sets empty_repo for project to passed value', () => {
mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: false });
expect(localState.projects.abcproject.empty_repo).toBe(false);
mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: true });
expect(localState.projects.abcproject.empty_repo).toBe(true);
});
});
});
...@@ -5,21 +5,53 @@ import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helpe ...@@ -5,21 +5,53 @@ import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helpe
import { file, resetStore } from '../helpers'; import { file, resetStore } from '../helpers';
import { projectData } from '../mock_data'; import { projectData } from '../mock_data';
describe('ide component', () => { function bootstrap(projData) {
const Component = Vue.extend(ide);
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projData);
Vue.set(store.state.trees, 'abcproject/master', {
tree: [],
loading: false,
});
return createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
noChangesStateSvgPath: 'svg',
committedStateSvgPath: 'svg',
});
}
describe('ide component, empty repo', () => {
let vm; let vm;
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(ide); const emptyProjData = Object.assign({}, projectData, { empty_repo: true, branches: {} });
vm = bootstrap(emptyProjData);
vm.$mount();
});
store.state.currentProjectId = 'abcproject'; afterEach(() => {
store.state.currentBranchId = 'master'; vm.$destroy();
store.state.projects.abcproject = Object.assign({}, projectData);
resetStore(vm.$store);
});
vm = createComponentWithStore(Component, store, { it('renders "New file" button in empty repo', done => {
emptyStateSvgPath: 'svg', vm.$nextTick(() => {
noChangesStateSvgPath: 'svg', expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).not.toBeNull();
committedStateSvgPath: 'svg', done();
}).$mount(); });
});
});
describe('ide component, non-empty repo', () => {
let vm;
beforeEach(() => {
vm = bootstrap(projectData);
vm.$mount();
}); });
afterEach(() => { afterEach(() => {
...@@ -28,17 +60,15 @@ describe('ide component', () => { ...@@ -28,17 +60,15 @@ describe('ide component', () => {
resetStore(vm.$store); resetStore(vm.$store);
}); });
it('does not render right when no files open', () => { it('shows error message when set', done => {
expect(vm.$el.querySelector('.panel-right')).toBeNull(); expect(vm.$el.querySelector('.flash-container')).toBe(null);
});
it('renders right panel when files are open', done => { vm.$store.state.errorMessage = {
vm.$store.state.trees['abcproject/mybranch'] = { text: 'error',
tree: [file()],
}; };
Vue.nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.querySelector('.panel-right')).toBeNull(); expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
done(); done();
}); });
...@@ -71,17 +101,25 @@ describe('ide component', () => { ...@@ -71,17 +101,25 @@ describe('ide component', () => {
}); });
}); });
it('shows error message when set', done => { describe('non-existent branch', () => {
expect(vm.$el.querySelector('.flash-container')).toBe(null); it('does not render "New file" button for non-existent branch when repo is not empty', done => {
vm.$nextTick(() => {
vm.$store.state.errorMessage = { expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
text: 'error', done();
}; });
});
});
vm.$nextTick(() => { describe('branch with files', () => {
expect(vm.$el.querySelector('.flash-container')).not.toBe(null); beforeEach(() => {
store.state.trees['abcproject/master'].tree = [file()];
});
done(); it('does not render "New file" button', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
done();
});
}); });
}); });
}); });
...@@ -7,25 +7,23 @@ import { projectData } from '../mock_data'; ...@@ -7,25 +7,23 @@ import { projectData } from '../mock_data';
describe('IDE tree list', () => { describe('IDE tree list', () => {
const Component = Vue.extend(IdeTreeList); const Component = Vue.extend(IdeTreeList);
const normalBranchTree = [file('fileName')];
const emptyBranchTree = [];
let vm; let vm;
beforeEach(() => { const bootstrapWithTree = (tree = normalBranchTree) => {
store.state.currentProjectId = 'abcproject'; store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master'; store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData); store.state.projects.abcproject = Object.assign({}, projectData);
Vue.set(store.state.trees, 'abcproject/master', { Vue.set(store.state.trees, 'abcproject/master', {
tree: [file('fileName')], tree,
loading: false, loading: false,
}); });
vm = createComponentWithStore(Component, store, { vm = createComponentWithStore(Component, store, {
viewerType: 'edit', viewerType: 'edit',
}); });
};
spyOn(vm, 'updateViewer').and.callThrough();
vm.$mount();
});
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.$destroy();
...@@ -33,22 +31,47 @@ describe('IDE tree list', () => { ...@@ -33,22 +31,47 @@ describe('IDE tree list', () => {
resetStore(vm.$store); resetStore(vm.$store);
}); });
it('updates viewer on mount', () => { describe('normal branch', () => {
expect(vm.updateViewer).toHaveBeenCalledWith('edit'); beforeEach(() => {
}); bootstrapWithTree();
spyOn(vm, 'updateViewer').and.callThrough();
vm.$mount();
});
it('updates viewer on mount', () => {
expect(vm.updateViewer).toHaveBeenCalledWith('edit');
});
it('renders loading indicator', done => {
store.state.trees['abcproject/master'].loading = true;
it('renders loading indicator', done => { vm.$nextTick(() => {
store.state.trees['abcproject/master'].loading = true; expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
vm.$nextTick(() => { done();
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull(); });
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3); });
done(); it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
}); });
}); });
it('renders list of files', () => { describe('empty-branch state', () => {
expect(vm.$el.textContent).toContain('fileName'); beforeEach(() => {
bootstrapWithTree(emptyBranchTree);
spyOn(vm, 'updateViewer').and.callThrough();
vm.$mount();
});
it('does not load files if the branch is empty', () => {
expect(vm.$el.textContent).not.toContain('fileName');
expect(vm.$el.textContent).toContain('No files');
});
}); });
}); });
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
refreshLastCommitData, refreshLastCommitData,
showBranchNotFoundError, showBranchNotFoundError,
createNewBranchFromDefault, createNewBranchFromDefault,
getBranchData, showEmptyState,
openBranch, openBranch,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import store from '~/ide/stores'; import store from '~/ide/stores';
...@@ -196,39 +196,44 @@ describe('IDE store project actions', () => { ...@@ -196,39 +196,44 @@ describe('IDE store project actions', () => {
}); });
}); });
describe('getBranchData', () => { describe('showEmptyState', () => {
describe('error', () => { it('commits proper mutations when supplied error is 404', done => {
it('dispatches branch not found action when response is 404', done => { testAction(
const dispatch = jasmine.createSpy('dispatchSpy'); showEmptyState,
{
mock.onGet(/(.*)/).replyOnce(404); err: {
response: {
getBranchData( status: 404,
},
},
projectId: 'abc/def',
branchId: 'master',
},
store.state,
[
{ {
commit() {}, type: 'CREATE_TREE',
dispatch, payload: {
state: store.state, treePath: 'abc/def/master',
},
}, },
{ {
projectId: 'abc/def', type: 'TOGGLE_LOADING',
branchId: 'master-testing', payload: {
entry: store.state.trees['abc/def/master'],
forceValue: false,
},
}, },
) ],
.then(done.fail) [],
.catch(() => { done,
expect(dispatch.calls.argsFor(0)).toEqual([ );
'showBranchNotFoundError',
'master-testing',
]);
done();
});
});
}); });
}); });
describe('openBranch', () => { describe('openBranch', () => {
const branch = { const branch = {
projectId: 'feature/lorem-ipsum', projectId: 'abc/def',
branchId: '123-lorem', branchId: '123-lorem',
}; };
...@@ -238,63 +243,113 @@ describe('IDE store project actions', () => { ...@@ -238,63 +243,113 @@ describe('IDE store project actions', () => {
'foo/bar-pending': { pending: true }, 'foo/bar-pending': { pending: true },
'foo/bar': { pending: false }, 'foo/bar': { pending: false },
}; };
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
}); });
it('dispatches branch actions', done => { describe('empty repo', () => {
openBranch(store, branch) beforeEach(() => {
.then(() => { spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['getBranchData', branch],
['getFiles', branch],
['getMergeRequestsForBranch', branch],
]);
})
.then(done)
.catch(done.fail);
});
it('handles tree entry action, if basePath is given', done => { store.state.currentProjectId = 'abc/def';
openBranch(store, { ...branch, basePath: 'foo/bar/' }) store.state.projects['abc/def'] = {
.then(() => { empty_repo: true,
expect(store.dispatch).toHaveBeenCalledWith( };
'handleTreeEntryAction', });
store.state.entries['foo/bar'],
); afterEach(() => {
}) resetStore(store);
.then(done) });
.catch(done.fail);
it('dispatches showEmptyState action right away', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['showEmptyState', branch],
]);
done();
})
.catch(done.fail);
});
}); });
it('does not handle tree entry action, if entry is pending', done => { describe('existing branch', () => {
openBranch(store, { ...branch, basePath: 'foo/bar-pending' }) beforeEach(() => {
.then(() => { spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
expect(store.dispatch).not.toHaveBeenCalledWith( });
'handleTreeEntryAction',
jasmine.anything(), it('dispatches branch actions', done => {
); openBranch(store, branch)
}) .then(() => {
.then(done) expect(store.dispatch.calls.allArgs()).toEqual([
.catch(done.fail); ['setCurrentBranchId', branch.branchId],
['getBranchData', branch],
['getMergeRequestsForBranch', branch],
['getFiles', branch],
]);
})
.then(done)
.catch(done.fail);
});
it('handles tree entry action, if basePath is given', done => {
openBranch(store, { ...branch, basePath: 'foo/bar/' })
.then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'handleTreeEntryAction',
store.state.entries['foo/bar'],
);
})
.then(done)
.catch(done.fail);
});
it('does not handle tree entry action, if entry is pending', done => {
openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
.then(() => {
expect(store.dispatch).not.toHaveBeenCalledWith(
'handleTreeEntryAction',
jasmine.anything(),
);
})
.then(done)
.catch(done.fail);
});
it('creates a new file supplied via URL if the file does not exist yet', done => {
openBranch(store, { ...branch, basePath: 'not-existent.md' })
.then(() => {
expect(store.dispatch).not.toHaveBeenCalledWith(
'handleTreeEntryAction',
jasmine.anything(),
);
expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
name: 'not-existent.md',
type: 'blob',
});
})
.then(done)
.catch(done.fail);
});
}); });
it('creates a new file supplied via URL if the file does not exist yet', done => { describe('non-existent branch', () => {
openBranch(store, { ...branch, basePath: 'not-existent.md' }) beforeEach(() => {
.then(() => { spyOn(store, 'dispatch').and.returnValue(Promise.reject());
expect(store.dispatch).not.toHaveBeenCalledWith( });
'handleTreeEntryAction',
jasmine.anything(),
);
expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { it('dispatches correct branch actions', done => {
name: 'not-existent.md', openBranch(store, branch)
type: 'blob', .then(() => {
}); expect(store.dispatch.calls.allArgs()).toEqual([
}) ['setCurrentBranchId', branch.branchId],
.then(done) ['getBranchData', branch],
.catch(done.fail); ['showBranchNotFoundError', branch.branchId],
]);
})
.then(done)
.catch(done.fail);
});
}); });
}); });
}); });
...@@ -93,38 +93,6 @@ describe('Multi-file store tree actions', () => { ...@@ -93,38 +93,6 @@ describe('Multi-file store tree actions', () => {
}); });
describe('error', () => { describe('error', () => {
it('dispatches branch not found actions when response is 404', done => {
const dispatch = jasmine.createSpy('dispatchSpy');
store.state.projects = {
'abc/def': {
web_url: `${gl.TEST_HOST}/files`,
},
};
mock.onGet(/(.*)/).replyOnce(404);
getFiles(
{
commit() {},
dispatch,
state: store.state,
},
{
projectId: 'abc/def',
branchId: 'master-testing',
},
)
.then(done.fail)
.catch(() => {
expect(dispatch.calls.argsFor(0)).toEqual([
'showBranchNotFoundError',
'master-testing',
]);
done();
});
});
it('dispatches error action', done => { it('dispatches error action', done => {
const dispatch = jasmine.createSpy('dispatchSpy'); const dispatch = jasmine.createSpy('dispatchSpy');
......
...@@ -9,12 +9,15 @@ import actions, { ...@@ -9,12 +9,15 @@ import actions, {
setErrorMessage, setErrorMessage,
deleteEntry, deleteEntry,
renameEntry, renameEntry,
getBranchData,
} from '~/ide/stores/actions'; } from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores'; import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types'; import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router'; import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers'; import { resetStore, file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper'; import testAction from '../../helpers/vuex_action_helper';
import MockAdapter from 'axios-mock-adapter';
describe('Multi-file store actions', () => { describe('Multi-file store actions', () => {
beforeEach(() => { beforeEach(() => {
...@@ -560,4 +563,65 @@ describe('Multi-file store actions', () => { ...@@ -560,4 +563,65 @@ describe('Multi-file store actions', () => {
); );
}); });
}); });
describe('getBranchData', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('error', () => {
let dispatch;
const callParams = [
{
commit() {},
state: store.state,
},
{
projectId: 'abc/def',
branchId: 'master-testing',
},
];
beforeEach(() => {
dispatch = jasmine.createSpy('dispatchSpy');
document.body.innerHTML += '<div class="flash-container"></div>';
});
afterEach(() => {
document.querySelector('.flash-container').remove();
});
it('passes the error further unchanged without dispatching any action when response is 404', done => {
mock.onGet(/(.*)/).replyOnce(404);
getBranchData(...callParams)
.then(done.fail)
.catch(e => {
expect(dispatch.calls.count()).toEqual(0);
expect(e.response.status).toEqual(404);
expect(document.querySelector('.flash-alert')).toBeNull();
done();
});
});
it('does not pass the error further and flashes an alert if error is not 404', done => {
mock.onGet(/(.*)/).replyOnce(418);
getBranchData(...callParams)
.then(done.fail)
.catch(e => {
expect(dispatch.calls.count()).toEqual(0);
expect(e.response).toBeUndefined();
expect(document.querySelector('.flash-alert')).not.toBeNull();
done();
});
});
});
});
}); });
...@@ -272,6 +272,7 @@ describe('IDE commit module actions', () => { ...@@ -272,6 +272,7 @@ describe('IDE commit module actions', () => {
short_id: '123', short_id: '123',
message: 'test message', message: 'test message',
committed_date: 'date', committed_date: 'date',
parent_ids: '321',
stats: { stats: {
additions: '1', additions: '1',
deletions: '2', deletions: '2',
...@@ -463,5 +464,63 @@ describe('IDE commit module actions', () => { ...@@ -463,5 +464,63 @@ describe('IDE commit module actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
}); });
describe('first commit of a branch', () => {
const COMMIT_RESPONSE = {
id: '123456',
short_id: '123',
message: 'test message',
committed_date: 'date',
parent_ids: [],
stats: {
additions: '1',
deletions: '2',
},
};
it('commits TOGGLE_EMPTY_STATE mutation on empty repo', done => {
spyOn(service, 'commit').and.returnValue(
Promise.resolve({
data: COMMIT_RESPONSE,
}),
);
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/commitChanges')
.then(() => {
expect(store.commit.calls.allArgs()).toEqual(
jasmine.arrayContaining([
['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)],
]),
);
done();
})
.catch(done.fail);
});
it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', done => {
COMMIT_RESPONSE.parent_ids.push('1234');
spyOn(service, 'commit').and.returnValue(
Promise.resolve({
data: COMMIT_RESPONSE,
}),
);
spyOn(store, 'commit').and.callThrough();
store
.dispatch('commit/commitChanges')
.then(() => {
expect(store.commit.calls.allArgs()).not.toEqual(
jasmine.arrayContaining([
['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)],
]),
);
done();
})
.catch(done.fail);
});
});
}); });
}); });
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