Commit df8a38b9 authored by Fatih Acet's avatar Fatih Acet

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

Port of 45687-web-ide-empty-state to EE

See merge request gitlab-org/gitlab-ee!10445
parents cd8859d4 84be2e96
<script>
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
......@@ -22,6 +23,8 @@ export default {
FindFile,
ErrorMessage,
CommitEditorHeader,
GlButton,
GlLoadingIcon,
},
props: {
rightPaneComponent: {
......@@ -47,13 +50,15 @@ export default {
'someUncommittedChanges',
'isCommitModeActive',
'allBlobs',
'emptyRepo',
'currentTree',
]),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
},
methods: {
...mapActions(['toggleFileFinder']),
...mapActions(['toggleFileFinder', 'openNewEntryModal']),
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
......@@ -98,17 +103,40 @@ export default {
<repo-editor :file="activeFile" class="multi-file-edit-pane-content" />
</template>
<template v-else>
<div v-once class="ide-empty-state">
<div class="ide-empty-state">
<div class="row js-empty-state">
<div class="col-12">
<div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div>
</div>
<div class="col-12">
<div class="text-content text-center">
<h4>Welcome to the GitLab IDE</h4>
<p>
Select a file from the left sidebar to begin editing. Afterwards, you'll be able
to commit your changes.
<h4>
{{ __('Make and review changes in the browser with the Web IDE') }}
</h4>
<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>
</div>
</div>
......
......@@ -54,14 +54,17 @@ export default {
<slot name="header"></slot>
</header>
<div class="ide-tree-body h-100">
<file-row
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:extra-component="$options.FileRowExtra"
@toggleTreeOpen="toggleTreeOpen"
/>
<template v-if="currentTree.tree.length">
<file-row
v-for="file in currentTree.tree"
:key="file.key"
:file="file"
:level="0"
:extra-component="$options.FileRowExtra"
@toggleTreeOpen="toggleTreeOpen"
/>
</template>
<div v-else class="file-row">{{ __('No files') }}</div>
</div>
</template>
</div>
......
import $ from 'jquery';
import Vue from 'vue';
import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
import _ from 'underscore';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
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);
......@@ -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/file';
export * from './actions/project';
......
......@@ -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 } = {}) =>
service
.getBranchData(projectId, branchId)
......@@ -125,40 +83,66 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => {
dispatch('setCurrentBranchId', branchId);
dispatch('getBranchData', {
projectId,
branchId,
export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
const treePath = `${projectId}/${branchId}`;
commit(types.CREATE_TREE, { treePath });
commit(types.TOGGLE_LOADING, {
entry: state.trees[treePath],
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,
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(() => {
dispatch('getMergeRequestsForBranch', {
projectId,
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 } =
resolve();
})
.catch(e => {
if (e.response.status === 404) {
dispatch('showBranchNotFoundError', branchId);
} else {
dispatch('setErrorMessage', {
text: __('An error occurred whilst loading all the files.'),
action: payload =>
dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
actionPayload: { projectId, branchId },
});
}
dispatch('setErrorMessage', {
text: __('An error occurred whilst loading all the files.'),
action: payload =>
dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
actionPayload: { projectId, branchId },
});
reject(e);
});
} else {
......
......@@ -36,6 +36,9 @@ export const currentMergeRequest = state => {
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 =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
......
......@@ -135,6 +135,17 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return null;
}
if (!data.parent_ids.length) {
commit(
rootTypes.TOGGLE_EMPTY_STATE,
{
projectPath: rootState.currentProjectId,
value: false,
},
{ root: true },
);
}
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
return dispatch('updateFilesAfterCommit', {
......
......@@ -12,6 +12,7 @@ export const SET_LINKS = 'SET_LINKS';
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
......
......@@ -19,6 +19,12 @@ export default {
});
},
[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], {
workingReference: reference,
});
......
......@@ -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
end
end
expose :empty_repo?, as: :empty_repo
expose :archived?, as: :archived
expose :visibility
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
......
......@@ -1211,6 +1211,9 @@ msgstr ""
msgid "An error occurred whilst fetching the latest pipeline."
msgstr ""
msgid "An error occurred whilst getting files for - %{branchId}"
msgstr ""
msgid "An error occurred whilst loading all the files."
msgstr ""
......@@ -1938,6 +1941,9 @@ msgstr ""
msgid "Branch name"
msgstr ""
msgid "Branch not loaded - %{branchId}"
msgstr ""
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
......@@ -3620,6 +3626,9 @@ msgstr ""
msgid "Create a new branch"
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"
msgstr ""
......@@ -7667,6 +7676,9 @@ msgstr ""
msgid "MRDiff|Show full file"
msgstr ""
msgid "Make and review changes in the browser with the Web IDE"
msgstr ""
msgid "Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos."
msgstr ""
......@@ -8500,6 +8512,9 @@ msgstr ""
msgid "No file selected"
msgstr ""
msgid "No files"
msgstr ""
msgid "No files found."
msgstr ""
......@@ -11252,6 +11267,9 @@ msgstr ""
msgid "Select Page"
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"
msgstr ""
......
......@@ -37,4 +37,39 @@ describe('Multi-file store branch mutations', () => {
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
import { file, resetStore } from '../helpers';
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;
beforeEach(() => {
const Component = Vue.extend(ide);
const emptyProjData = Object.assign({}, projectData, { empty_repo: true, branches: {} });
vm = bootstrap(emptyProjData);
vm.$mount();
});
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
vm = createComponentWithStore(Component, store, {
emptyStateSvgPath: 'svg',
noChangesStateSvgPath: 'svg',
committedStateSvgPath: 'svg',
}).$mount();
it('renders "New file" button in empty repo', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).not.toBeNull();
done();
});
});
});
describe('ide component, non-empty repo', () => {
let vm;
beforeEach(() => {
vm = bootstrap(projectData);
vm.$mount();
});
afterEach(() => {
......@@ -28,17 +60,15 @@ describe('ide component', () => {
resetStore(vm.$store);
});
it('does not render right when no files open', () => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
});
it('shows error message when set', done => {
expect(vm.$el.querySelector('.flash-container')).toBe(null);
it('renders right panel when files are open', done => {
vm.$store.state.trees['abcproject/mybranch'] = {
tree: [file()],
vm.$store.state.errorMessage = {
text: 'error',
};
Vue.nextTick(() => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
vm.$nextTick(() => {
expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
done();
});
......@@ -71,17 +101,25 @@ describe('ide component', () => {
});
});
it('shows error message when set', done => {
expect(vm.$el.querySelector('.flash-container')).toBe(null);
vm.$store.state.errorMessage = {
text: 'error',
};
describe('non-existent branch', () => {
it('does not render "New file" button for non-existent branch when repo is not empty', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
done();
});
});
});
vm.$nextTick(() => {
expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
describe('branch with files', () => {
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';
describe('IDE tree list', () => {
const Component = Vue.extend(IdeTreeList);
const normalBranchTree = [file('fileName')];
const emptyBranchTree = [];
let vm;
beforeEach(() => {
const bootstrapWithTree = (tree = normalBranchTree) => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
Vue.set(store.state.trees, 'abcproject/master', {
tree: [file('fileName')],
tree,
loading: false,
});
vm = createComponentWithStore(Component, store, {
viewerType: 'edit',
});
spyOn(vm, 'updateViewer').and.callThrough();
vm.$mount();
});
};
afterEach(() => {
vm.$destroy();
......@@ -33,22 +31,47 @@ describe('IDE tree list', () => {
resetStore(vm.$store);
});
it('updates viewer on mount', () => {
expect(vm.updateViewer).toHaveBeenCalledWith('edit');
});
describe('normal branch', () => {
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 => {
store.state.trees['abcproject/master'].loading = true;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
vm.$nextTick(() => {
expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
done();
});
});
done();
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
});
});
it('renders list of files', () => {
expect(vm.$el.textContent).toContain('fileName');
describe('empty-branch state', () => {
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 {
refreshLastCommitData,
showBranchNotFoundError,
createNewBranchFromDefault,
getBranchData,
showEmptyState,
openBranch,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
......@@ -196,39 +196,44 @@ describe('IDE store project actions', () => {
});
});
describe('getBranchData', () => {
describe('error', () => {
it('dispatches branch not found action when response is 404', done => {
const dispatch = jasmine.createSpy('dispatchSpy');
mock.onGet(/(.*)/).replyOnce(404);
getBranchData(
describe('showEmptyState', () => {
it('commits proper mutations when supplied error is 404', done => {
testAction(
showEmptyState,
{
err: {
response: {
status: 404,
},
},
projectId: 'abc/def',
branchId: 'master',
},
store.state,
[
{
commit() {},
dispatch,
state: store.state,
type: 'CREATE_TREE',
payload: {
treePath: 'abc/def/master',
},
},
{
projectId: 'abc/def',
branchId: 'master-testing',
type: 'TOGGLE_LOADING',
payload: {
entry: store.state.trees['abc/def/master'],
forceValue: false,
},
},
)
.then(done.fail)
.catch(() => {
expect(dispatch.calls.argsFor(0)).toEqual([
'showBranchNotFoundError',
'master-testing',
]);
done();
});
});
],
[],
done,
);
});
});
describe('openBranch', () => {
const branch = {
projectId: 'feature/lorem-ipsum',
projectId: 'abc/def',
branchId: '123-lorem',
};
......@@ -238,63 +243,113 @@ describe('IDE store project actions', () => {
'foo/bar-pending': { pending: true },
'foo/bar': { pending: false },
};
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
});
it('dispatches branch actions', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['getBranchData', branch],
['getFiles', branch],
['getMergeRequestsForBranch', branch],
]);
})
.then(done)
.catch(done.fail);
});
describe('empty repo', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
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);
store.state.currentProjectId = 'abc/def';
store.state.projects['abc/def'] = {
empty_repo: true,
};
});
afterEach(() => {
resetStore(store);
});
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 => {
openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
.then(() => {
expect(store.dispatch).not.toHaveBeenCalledWith(
'handleTreeEntryAction',
jasmine.anything(),
);
})
.then(done)
.catch(done.fail);
describe('existing branch', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
});
it('dispatches branch actions', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['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 => {
openBranch(store, { ...branch, basePath: 'not-existent.md' })
.then(() => {
expect(store.dispatch).not.toHaveBeenCalledWith(
'handleTreeEntryAction',
jasmine.anything(),
);
describe('non-existent branch', () => {
beforeEach(() => {
spyOn(store, 'dispatch').and.returnValue(Promise.reject());
});
expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
name: 'not-existent.md',
type: 'blob',
});
})
.then(done)
.catch(done.fail);
it('dispatches correct branch actions', done => {
openBranch(store, branch)
.then(() => {
expect(store.dispatch.calls.allArgs()).toEqual([
['setCurrentBranchId', branch.branchId],
['getBranchData', branch],
['showBranchNotFoundError', branch.branchId],
]);
})
.then(done)
.catch(done.fail);
});
});
});
});
......@@ -93,38 +93,6 @@ describe('Multi-file store tree actions', () => {
});
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 => {
const dispatch = jasmine.createSpy('dispatchSpy');
......
......@@ -9,12 +9,15 @@ import actions, {
setErrorMessage,
deleteEntry,
renameEntry,
getBranchData,
} from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper';
import MockAdapter from 'axios-mock-adapter';
describe('Multi-file store actions', () => {
beforeEach(() => {
......@@ -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', () => {
short_id: '123',
message: 'test message',
committed_date: 'date',
parent_ids: '321',
stats: {
additions: '1',
deletions: '2',
......@@ -463,5 +464,63 @@ describe('IDE commit module actions', () => {
.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