Commit ecc821a9 authored by Phil Hughes's avatar Phil Hughes
Browse files

Merge branch '44833-ide-clean-up-status-bar' into 'master'

Resolve "Clean up bottom status bar Web IDE"

Closes #44833

See merge request gitlab-org/gitlab-ce!18756
parents 68b71df6 b7f3d747
...@@ -65,61 +65,63 @@ export default { ...@@ -65,61 +65,63 @@ export default {
</script> </script>
<template> <template>
<div <article class="ide">
class="ide-view"
>
<find-file
v-show="fileFindVisible"
/>
<ide-sidebar />
<div <div
class="multi-file-edit-pane" class="ide-view"
> >
<template <find-file
v-if="activeFile" v-show="fileFindVisible"
> />
<repo-tabs <ide-sidebar />
:active-file="activeFile" <div
:files="openFiles" class="multi-file-edit-pane"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
<ide-status-bar
:file="activeFile"
/>
</template>
<template
v-else
> >
<div <template
v-once v-if="activeFile"
class="ide-empty-state" >
<repo-tabs
:active-file="activeFile"
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
:file="activeFile"
/>
</template>
<template
v-else
> >
<div class="row js-empty-state"> <div
<div class="col-xs-12"> v-once
<div class="svg-content svg-250"> class="ide-empty-state"
<img :src="emptyStateSvgPath" /> >
<div class="row js-empty-state">
<div class="col-xs-12">
<div class="svg-content svg-250">
<img :src="emptyStateSvgPath" />
</div>
</div> </div>
</div> <div class="col-xs-12">
<div class="col-xs-12"> <div class="text-content text-center">
<div class="text-content text-center"> <h4>
<h4> Welcome to the GitLab IDE
Welcome to the GitLab IDE </h4>
</h4> <p>
<p> You can select a file in the left sidebar to begin
You can select a file in the left sidebar to begin editing and use the right sidebar to commit your changes.
editing and use the right sidebar to commit your changes. </p>
</p> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</template> </div>
</div> </div>
</div> <ide-status-bar
:file="activeFile"
/>
</article>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import timeAgoMixin from '~/vue_shared/mixins/timeago'; import timeAgoMixin from '~/vue_shared/mixins/timeago';
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
export default { export default {
components: { components: {
icon, icon,
userAvatarImage,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -14,40 +17,93 @@ export default { ...@@ -14,40 +17,93 @@ export default {
props: { props: {
file: { file: {
type: Object, type: Object,
required: true, required: false,
default: null,
},
},
data() {
return {
lastCommitFormatedAge: null,
};
},
computed: {
...mapGetters(['currentProject', 'lastCommit']),
},
mounted() {
this.startTimer();
},
beforeDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
},
methods: {
startTimer() {
this.intervalId = setInterval(() => {
this.commitAgeUpdate();
}, 1000);
},
commitAgeUpdate() {
if (this.lastCommit) {
this.lastCommitFormatedAge = this.timeFormated(this.lastCommit.committed_date);
}
},
getCommitPath(shortSha) {
return `${this.currentProject.web_url}/commit/${shortSha}`;
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="ide-status-bar"> <footer class="ide-status-bar">
<div> <div
<div v-if="file.lastCommit && file.lastCommit.id"> class="ide-status-branch"
Last commit: v-if="lastCommit && lastCommitFormatedAge"
<a >
v-tooltip <icon
:title="file.lastCommit.message" name="commit"
:href="file.lastCommit.url" />
> <a
{{ timeFormated(file.lastCommit.updatedAt) }} by v-tooltip
{{ file.lastCommit.author }} class="commit-sha"
</a> :title="lastCommit.message"
</div> :href="getCommitPath(lastCommit.short_id)"
>{{ lastCommit.short_id }}</a>
by
{{ lastCommit.author_name }}
<time
v-tooltip
data-placement="top"
data-container="body"
:datetime="lastCommit.committed_date"
:title="tooltipTitle(lastCommit.committed_date)"
>
{{ lastCommitFormatedAge }}
</time>
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.name }} {{ file.name }}
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.eol }} {{ file.eol }}
</div> </div>
<div <div
class="text-right" class="ide-status-file"
v-if="!file.binary"> v-if="file && !file.binary">
{{ file.editorRow }}:{{ file.editorColumn }} {{ file.editorRow }}:{{ file.editorColumn }}
</div> </div>
<div class="text-right"> <div
v-if="file"
class="ide-status-file"
>
{{ file.fileLanguage }} {{ file.fileLanguage }}
</div> </div>
</div> </footer>
</template> </template>
...@@ -72,3 +72,26 @@ export const getBranchData = ( ...@@ -72,3 +72,26 @@ export const getBranchData = (
resolve(state.projects[`${projectId}`].branches[branchId]); resolve(state.projects[`${projectId}`].branches[branchId]);
} }
}); });
export const refreshLastCommitData = (
{ commit, state, dispatch },
{ projectId, branchId } = {},
) => service
.getBranchData(projectId, branchId)
.then(({ data }) => {
commit(types.SET_BRANCH_COMMIT, {
projectId,
branchId,
commit: data.commit,
});
})
.catch(() => {
flash(
'Error loading last commit.',
'alert',
document,
null,
false,
true,
);
});
...@@ -81,5 +81,11 @@ export const getUnstagedFilesCountForPath = state => path => ...@@ -81,5 +81,11 @@ export const getUnstagedFilesCountForPath = state => path =>
export const getStagedFilesCountForPath = state => path => export const getStagedFilesCountForPath = state => path =>
getChangesCountForFiles(state.stagedFiles, path); getChangesCountForFiles(state.stagedFiles, path);
export const lastCommit = (state, getters) => {
const branch = getters.currentProject && getters.currentProject.branches[state.currentBranchId];
return branch ? branch.commit : null;
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
...@@ -210,7 +210,11 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ...@@ -210,7 +210,11 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
); );
} }
}) })
.then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH)); .then(() => dispatch('updateCommitAction', consts.COMMIT_TO_CURRENT_BRANCH))
.then(() => dispatch('refreshLastCommitData', {
projectId: rootState.currentProjectId,
branchId: rootState.currentBranchId,
}, { root: true }));
}) })
.catch(err => { .catch(err => {
let errMsg = __('Error committing changes. Please try again.'); let errMsg = __('Error committing changes. Please try again.');
......
...@@ -20,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS'; ...@@ -20,6 +20,7 @@ export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types // Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
......
...@@ -23,4 +23,9 @@ export default { ...@@ -23,4 +23,9 @@ export default {
workingReference: reference, workingReference: reference,
}); });
}, },
[types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) {
Object.assign(state.projects[projectId].branches[branchId], {
commit,
});
},
}; };
...@@ -230,6 +230,7 @@ $row-hover: $blue-50; ...@@ -230,6 +230,7 @@ $row-hover: $blue-50;
$row-hover-border: $blue-200; $row-hover-border: $blue-200;
$progress-color: #c0392b; $progress-color: #c0392b;
$header-height: 40px; $header-height: 40px;
$ide-statusbar-height: 27px;
$fixed-layout-width: 1280px; $fixed-layout-width: 1280px;
$limited-layout-width: 990px; $limited-layout-width: 990px;
$limited-layout-width-sm: 790px; $limited-layout-width-sm: 790px;
......
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
margin-top: 0; margin-top: 0;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
padding-bottom: $ide-statusbar-height;
&.is-collapsed { &.is-collapsed {
.ide-file-list { .ide-file-list {
...@@ -375,7 +376,13 @@ ...@@ -375,7 +376,13 @@
padding: $gl-bar-padding $gl-padding; padding: $gl-bar-padding $gl-padding;
background: $white-light; background: $white-light;
display: flex; display: flex;
justify-content: flex-end; justify-content: space-between;
height: $ide-statusbar-height;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
> div + div { > div + div {
padding-left: $gl-padding; padding-left: $gl-padding;
...@@ -386,6 +393,14 @@ ...@@ -386,6 +393,14 @@
} }
} }
.ide-status-file {
text-align: right;
.ide-status-branch + &,
&:first-child {
margin-left: auto;
}
}
// Not great, but this is to deal with our current output // Not great, but this is to deal with our current output
.multi-file-preview-holder { .multi-file-preview-holder {
height: 100%; height: 100%;
......
---
title: Clean up WebIDE status bar and add useful info
merge_request:
author:
type: changed
import Vue from 'vue';
import store from '~/ide/stores';
import ideStatusBar from '~/ide/components/ide_status_bar.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
import { projectData } from '../mock_data';
describe('ideStatusBar', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(ideStatusBar);
store.state.currentProjectId = 'abcproject';
store.state.projects.abcproject = projectData;
vm = createComponentWithStore(Component, store).$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(vm.$store);
});
it('renders the statusbar', () => {
expect(vm.$el.className).toBe('ide-status-bar');
});
describe('mounted', () => {
it('triggers a setInterval', () => {
expect(vm.intervalId).not.toBe(null);
});
});
describe('commitAgeUpdate', () => {
beforeEach(function() {
jasmine.clock().install();
spyOn(vm, 'commitAgeUpdate').and.callFake(() => {});
vm.startTimer();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('gets called every second', () => {
expect(vm.commitAgeUpdate).not.toHaveBeenCalled();
jasmine.clock().tick(1100);
expect(vm.commitAgeUpdate.calls.count()).toEqual(1);
jasmine.clock().tick(1000);
expect(vm.commitAgeUpdate.calls.count()).toEqual(2);
});
});
describe('getCommitPath', () => {
it('returns the path to the commit details', () => {
expect(vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
});
});
});
import {
refreshLastCommitData,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
import { resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store project actions', () => {
beforeEach(() => {
store.state.projects.abcproject = {};
});
afterEach(() => {
resetStore(store);
});
describe('refreshLastCommitData', () => {
beforeEach(() => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
branches: {
master: {
commit: null,
},
},
};
});
it('calls the service', done => {
spyOn(service, 'getBranchData').and.returnValue(
Promise.resolve({
data: {
commit: { id: '123' },
},
}),
);
store
.dispatch('refreshLastCommitData', {
projectId: store.state.currentProjectId,
branchId: store.state.currentBranchId,
})
.then(() => {
expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
done();
})
.catch(done.fail);
});
it('commits getBranchData', done => {
testAction(
refreshLastCommitData,
{},
{},
[{
type: 'SET_BRANCH_COMMIT',
payload: {
projectId: 'abcproject',
branchId: 'master',
commit: { id: '123' },
},
}], // mutations
[], // action
done,
);
});
});
});
...@@ -141,4 +141,24 @@ describe('IDE store getters', () => { ...@@ -141,4 +141,24 @@ describe('IDE store getters', () => {
expect(getters.getChangesInFolder(localState)('test')).toBe(2); expect(getters.getChangesInFolder(localState)('test')).toBe(2);
}); });
}); });
describe('lastCommit', () => {
it('returns the last commit of the current branch on the current project', () => {
const commitTitle = 'Example commit title';
const localGetters = {
currentProject: {
branches: {
'example-branch': {
commit: {
title: commitTitle,
},
},
},
},
};
localState.currentBranchId = 'example-branch';
expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle);
});
});
}); });
...@@ -15,4 +15,26 @@ describe('Multi-file store branch mutations', () => { ...@@ -15,4 +15,26 @@ describe('Multi-file store branch mutations', () => {
expect(localState.currentBranchId).toBe('master'); expect(localState.currentBranchId).toBe('master');
}); });
});