added tab component
This commit is contained in:
parent
32f965b244
commit
cfe4d2f29d
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -20,3 +20,7 @@ export const viewerTypes = {
|
|||
edit: 'editor',
|
||||
diff: 'diff',
|
||||
};
|
||||
|
||||
export const rightSidebarViews = {
|
||||
pipelines: 'pipelines-list',
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -33,6 +33,7 @@ export default {
|
|||
if (!stage) {
|
||||
stage = {
|
||||
title: job.stage,
|
||||
isCollapsed: false,
|
||||
jobs: [],
|
||||
};
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -148,6 +148,9 @@ export default {
|
|||
unusedSeal: false,
|
||||
});
|
||||
},
|
||||
[types.SET_RIGHT_PANE](state, view) {
|
||||
state.rightPane = state.rightPane === view ? null : view;
|
||||
},
|
||||
...projectMutations,
|
||||
...mergeRequestMutation,
|
||||
...fileMutations,
|
||||
|
|
|
@ -23,4 +23,5 @@ export default () => ({
|
|||
currentActivityView: activityBarViews.edit,
|
||||
unusedSeal: true,
|
||||
fileFindVisible: false,
|
||||
rightPane: null,
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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]);
|
||||
},
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue