diff options
21 files changed, 620 insertions, 244 deletions
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 9894ebb0624..e41b1530226 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,6 +1,7 @@ <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> diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 81374f26645..95782b2c88a 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -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> diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index fd678e6e10c..dc8ca732879 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -1,12 +1,15 @@ 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'; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 4b10d148ebf..dd8f17e4f3a 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -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); }); }; diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js index 3d83e4a9ba5..75511574d3e 100644 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ b/app/assets/javascripts/ide/stores/actions/tree.js @@ -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 { diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 490658a4543..7a88ac5b116 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -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}`]; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index c2760eb1554..f81bdb8a30e 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -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', { diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index a5f8098dc17..86ab76136df 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -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'; diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js index e09f88878f4..6afd8de2aa4 100644 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ b/app/assets/javascripts/ide/stores/mutations/branch.js @@ -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, }); diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js index 284b39a2c72..9230f3839c1 100644 --- a/app/assets/javascripts/ide/stores/mutations/project.js +++ b/app/assets/javascripts/ide/stores/mutations/project.js @@ -21,4 +21,9 @@ export default { }), }); }, + [types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) { + Object.assign(state.projects[projectPath], { + empty_repo: value, + }); + }, }; diff --git a/changelogs/unreleased/45687-web-ide-empty-state.yml b/changelogs/unreleased/45687-web-ide-empty-state.yml new file mode 100644 index 00000000000..9ef148275ab --- /dev/null +++ b/changelogs/unreleased/45687-web-ide-empty-state.yml @@ -0,0 +1,5 @@ +--- +title: Empty project state for Web IDE +merge_request: 26556 +author: +type: added diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 96a1ccefbe5..3d7cf2c0bb1 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -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 } diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b6025f70e8a..af53da84bea 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -952,6 +952,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 "" @@ -1482,6 +1485,9 @@ msgstr "" msgid "Branch name" msgstr "" +msgid "Branch not loaded - %{branchId}" +msgstr "" + msgid "BranchSwitcherPlaceholder|Search branches" msgstr "" @@ -2993,6 +2999,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 "" @@ -5796,6 +5805,9 @@ msgstr "" msgid "MRDiff|Show full file" msgstr "" +msgid "Make and review changes in the browser with the Web IDE" +msgstr "" + msgid "Make issue confidential." msgstr "" @@ -6452,6 +6464,9 @@ msgstr "" msgid "No file selected" msgstr "" +msgid "No files" +msgstr "" + msgid "No files found." msgstr "" @@ -8615,6 +8630,9 @@ msgstr "" msgid "Select Archive Format" 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 "" diff --git a/spec/frontend/ide/stores/mutations/branch_spec.js b/spec/frontend/ide/stores/mutations/branch_spec.js index 29eb859ddaf..0900b25d5d3 100644 --- a/spec/frontend/ide/stores/mutations/branch_spec.js +++ b/spec/frontend/ide/stores/mutations/branch_spec.js @@ -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'); + }); + }); }); diff --git a/spec/frontend/ide/stores/mutations/project_spec.js b/spec/frontend/ide/stores/mutations/project_spec.js new file mode 100644 index 00000000000..b3ce39c33d2 --- /dev/null +++ b/spec/frontend/ide/stores/mutations/project_spec.js @@ -0,0 +1,23 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js index dc5790f6562..de4becec1cd 100644 --- a/spec/javascripts/ide/components/ide_spec.js +++ b/spec/javascripts/ide/components/ide_spec.js @@ -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(); + }); }); }); }); diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js index 4ecbdb8a55e..f63007c7dd2 100644 --- a/spec/javascripts/ide/components/ide_tree_list_spec.js +++ b/spec/javascripts/ide/components/ide_tree_list_spec.js @@ -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'); + }); }); }); diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index cd519eaed7c..8ecb6129c63 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -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); + }); }); }); }); diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js index 5ed9b9003a7..674ecdc6764 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -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'); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 0b5587d02ae..04e236fb042 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -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(); + }); + }); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index cdeb9b4b896..4413a12fac4 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -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); + }); + }); }); }); |