added tab component

This commit is contained in:
Phil Hughes 2018-05-23 11:44:47 +01:00
parent 32f965b244
commit cfe4d2f29d
No known key found for this signature in database
GPG Key ID: 32245528C52E0F9F
15 changed files with 314 additions and 16 deletions

View File

@ -6,7 +6,7 @@ import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
import RightSidebar from './right_sidebar/index.vue';
import RightPane from './panes/right.vue';
const originalStopCallback = Mousetrap.stopCallback;
@ -17,7 +17,7 @@ export default {
IdeStatusBar,
RepoEditor,
FindFile,
RightSidebar,
RightPane,
},
computed: {
...mapState([
@ -125,7 +125,7 @@ export default {
</div>
</template>
</div>
<right-sidebar
<right-pane
v-if="currentProjectId"
/>
</div>

View File

@ -1,7 +1,9 @@
<script>
import { mapActions, mapState } from 'vuex';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import Pipelines from './pipelines.vue';
import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue';
export default {
directives: {
@ -9,8 +11,15 @@ export default {
},
components: {
Icon,
Pipelines,
PipelinesList,
},
computed: {
...mapState(['rightPane']),
},
methods: {
...mapActions(['setRightPane']),
},
rightSidebarViews,
};
</script>
@ -18,25 +27,31 @@ export default {
<div
class="multi-file-commit-panel ide-right-sidebar"
>
<div class="multi-file-commit-panel-inner">
<pipelines />
<div
class="multi-file-commit-panel-inner"
v-if="rightPane"
>
<keep-alive>
<component :is="rightPane" />
</keep-alive>
</div>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li v-once>
<a
<button
v-tooltip
data-container="body"
data-placement="left"
:title="__('Pipelines')"
class="ide-sidebar-link"
href="a"
type="button"
@click="setRightPane($options.rightSidebarViews.pipelines)"
>
<icon
:size="16"
name="log"
name="pipeline"
/>
</a>
</button>
</li>
</ul>
</nav>
@ -55,6 +70,7 @@ export default {
.ide-right-sidebar .multi-file-commit-panel-inner {
width: 300px;
padding: 8px 16px;
background-color: #fff;
border-left: 1px solid #eaeaea;
}

View File

@ -0,0 +1,40 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import Tabs from '../../../vue_shared/components/tabs/tabs';
import Tab from '../../../vue_shared/components/tabs/tab.vue';
export default {
components: {
Tabs,
Tab,
},
computed: {
...mapGetters('pipelines', ['jobsCount', 'failedJobs']),
},
mounted() {
this.fetchJobs();
},
methods: {
...mapActions('pipelines', ['fetchJobs']),
},
};
</script>
<template>
<div>
<tabs>
<tab active>
<template slot="title">
Jobs <span class="badge">{{ jobsCount }}</span>
</template>
List all jobs here
</tab>
<tab>
<template slot="title">
Failed Jobs <span class="badge">{{ failedJobs.length }}</span>
</template>
List all failed jobs here
</tab>
</tabs>
</div>
</template>

View File

@ -1,14 +1,17 @@
<script>
import { mapActions, mapState } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import JobsList from './jobs.vue';
export default {
components: {
LoadingIcon,
CiIcon,
JobsList,
},
computed: {
...mapGetters(['currentProject']),
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline']),
statusIcon() {
return {
@ -34,13 +37,31 @@ export default {
size="2"
/>
<template v-else-if="latestPipeline">
<ci-icon
:status="statusIcon"
/>
#{{ latestPipeline.id }}
<header
class="ide-tree-header ide-pipeline-header"
>
<ci-icon
:status="statusIcon"
/>
<span class="prepend-left-8">
<strong>
Pipeline
</strong>
<a
:href="currentProject.web_url + '/pipelines/' + latestPipeline.id"
target="_blank"
>
#{{ latestPipeline.id }}
</a>
</span>
</header>
<jobs-list />
</template>
</div>
</template>
<style>
.ide-pipeline-header .ci-status-icon {
display: flex;
}
</style>

View File

@ -20,3 +20,7 @@ export const viewerTypes = {
edit: 'editor',
diff: 'diff',
};
export const rightSidebarViews = {
pipelines: 'pipelines-list',
};

View File

@ -169,6 +169,10 @@ export const burstUnusedSeal = ({ state, commit }) => {
}
};
export const setRightPane = ({ commit }, view) => {
commit(types.SET_RIGHT_PANE, view);
};
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';

View File

@ -5,3 +5,5 @@ export const failedJobs = state =>
(acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
[],
);
export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);

View File

@ -33,6 +33,7 @@ export default {
if (!stage) {
stage = {
title: job.stage,
isCollapsed: false,
jobs: [],
};

View File

@ -65,3 +65,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';

View File

@ -148,6 +148,9 @@ export default {
unusedSeal: false,
});
},
[types.SET_RIGHT_PANE](state, view) {
state.rightPane = state.rightPane === view ? null : view;
},
...projectMutations,
...mergeRequestMutation,
...fileMutations,

View File

@ -23,4 +23,5 @@ export default () => ({
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
rightPane: null,
});

View File

@ -0,0 +1,42 @@
<script>
export default {
props: {
title: {
type: String,
required: false,
default: '',
},
active: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
// props can't be updated, so we map it to data where we can
localActive: this.active,
};
},
watch: {
active() {
this.localActive = this.active;
},
},
created() {
this.isTab = true;
},
};
</script>
<template>
<div
class="tab-pane"
:class="{
active: localActive
}"
role="tabpanel"
>
<slot></slot>
</div>
</template>

View File

@ -0,0 +1,62 @@
export default {
data() {
return {
currentIndex: 0,
tabs: [],
};
},
mounted() {
this.updateTabs();
},
methods: {
updateTabs() {
this.tabs = this.$children.filter(child => child.isTab);
this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
},
setTab(index) {
this.tabs[this.currentIndex].localActive = false;
this.tabs[index].localActive = true;
this.currentIndex = index;
},
},
render(h) {
const navItems = this.tabs.map((tab, i) =>
h(
'li',
{
key: i,
class: tab.localActive ? 'active' : null,
},
[
h(
'a',
{
href: '#',
on: {
click: () => this.setTab(i),
},
},
tab.$slots.title || tab.title,
),
],
),
);
const nav = h(
'ul',
{
class: 'nav-links tab-links',
},
[navItems],
);
const content = h(
'div',
{
class: ['tab-content'],
},
[this.$slots.default],
);
return h('div', {}, [[nav], content]);
},
};

View File

@ -0,0 +1,32 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import Tab from '~/vue_shared/components/tabs/tab.vue';
describe('Tab component', () => {
const Component = Vue.extend(Tab);
let vm;
beforeEach(() => {
vm = mountComponent(Component);
});
it('sets localActive to equal active', done => {
vm.active = true;
vm.$nextTick(() => {
expect(vm.localActive).toBe(true);
done();
});
});
it('sets active class', done => {
vm.active = true;
vm.$nextTick(() => {
expect(vm.$el.classList).toContain('active');
done();
});
});
});

View File

@ -0,0 +1,68 @@
import Vue from 'vue';
import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
describe('Tabs component', () => {
let vm;
beforeEach(done => {
vm = new Vue({
components: {
Tabs,
Tab,
},
template: `
<div>
<tabs>
<tab title="Testing" active>
First tab
</tab>
<tab>
<template slot="title">Test slot</template>
Second tab
</tab>
</tabs>
</div>
`,
}).$mount();
setTimeout(done);
});
describe('tab links', () => {
it('renders links for tabs', () => {
expect(vm.$el.querySelectorAll('a').length).toBe(2);
});
it('renders link titles from props', () => {
expect(vm.$el.querySelector('a').textContent).toContain('Testing');
});
it('renders link titles from slot', () => {
expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
});
it('renders active class', () => {
expect(vm.$el.querySelector('li').classList).toContain('active');
});
it('updates active class on click', done => {
vm.$el.querySelectorAll('a')[1].click();
setTimeout(() => {
expect(vm.$el.querySelector('li').classList).not.toContain('active');
expect(vm.$el.querySelectorAll('li')[1].classList).toContain('active');
done();
});
});
});
describe('content', () => {
it('renders content panes', () => {
expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
});
});
});