Merge branch '13076-stage-card-ui-component-ce' into 'master'
Stage card ui component See merge request gitlab-org/gitlab-ce!31580
This commit is contained in:
commit
224db2f890
|
@ -0,0 +1,41 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'StageCardListItem',
|
||||
components: {
|
||||
Icon,
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ active: isActive }" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded">
|
||||
<slot></slot>
|
||||
<div v-if="canEdit" class="dropdown">
|
||||
<gl-button
|
||||
:title="__('More actions')"
|
||||
class="more-actions-toggle btn btn-transparent p-0"
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
<icon css-classes="icon" name="ellipsis_v" />
|
||||
</gl-button>
|
||||
<ul class="more-actions-dropdown dropdown-menu dropdown-open-left">
|
||||
<slot name="dropdown-options"></slot>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,88 @@
|
|||
<script>
|
||||
import StageCardListItem from './stage_card_list_item.vue';
|
||||
|
||||
export default {
|
||||
name: 'StageNavItem',
|
||||
components: {
|
||||
StageCardListItem,
|
||||
},
|
||||
props: {
|
||||
isDefaultStage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
isUserAllowed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
},
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasValue() {
|
||||
return this.value && this.value.length > 0;
|
||||
},
|
||||
editable() {
|
||||
return this.isUserAllowed && this.canEdit;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li @click="$emit('select')">
|
||||
<stage-card-list-item :is-active="isActive" :can-edit="editable">
|
||||
<div class="stage-nav-item-cell stage-name p-0" :class="{ 'font-weight-bold': isActive }">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="stage-nav-item-cell stage-median mr-4">
|
||||
<template v-if="isUserAllowed">
|
||||
<span v-if="hasValue">{{ value }}</span>
|
||||
<span v-else class="stage-empty">{{ __('Not enough data') }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="not-available">{{ __('Not available') }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<template v-slot:dropdown-options>
|
||||
<template v-if="isDefaultStage">
|
||||
<li>
|
||||
<button type="button" class="btn-default btn-transparent">
|
||||
{{ __('Hide stage') }}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li>
|
||||
<button type="button" class="btn-default btn-transparent">
|
||||
{{ __('Edit stage') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="btn-danger danger">
|
||||
{{ __('Remove stage') }}
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
</stage-card-list-item>
|
||||
</li>
|
||||
</template>
|
|
@ -12,6 +12,7 @@ import stageComponent from './components/stage_component.vue';
|
|||
import stageReviewComponent from './components/stage_review_component.vue';
|
||||
import stageStagingComponent from './components/stage_staging_component.vue';
|
||||
import stageTestComponent from './components/stage_test_component.vue';
|
||||
import stageNavItem from './components/stage_nav_item.vue';
|
||||
import CycleAnalyticsService from './cycle_analytics_service';
|
||||
import CycleAnalyticsStore from './cycle_analytics_store';
|
||||
|
||||
|
@ -41,6 +42,7 @@ export default () => {
|
|||
import('ee_component/analytics/shared/components/projects_dropdown_filter.vue'),
|
||||
DateRangeDropdown: () =>
|
||||
import('ee_component/analytics/shared/components/date_range_dropdown.vue'),
|
||||
'stage-nav-item': stageNavItem,
|
||||
},
|
||||
mixins: [filterMixins],
|
||||
data() {
|
||||
|
|
|
@ -51,27 +51,19 @@
|
|||
}
|
||||
|
||||
.stage-header {
|
||||
width: 26%;
|
||||
padding-left: $gl-padding;
|
||||
width: 18.5%;
|
||||
}
|
||||
|
||||
.median-header {
|
||||
width: 14%;
|
||||
width: 21.5%;
|
||||
}
|
||||
|
||||
.event-header {
|
||||
width: 45%;
|
||||
padding-left: $gl-padding;
|
||||
}
|
||||
|
||||
.total-time-header {
|
||||
width: 15%;
|
||||
text-align: right;
|
||||
padding-right: $gl-padding;
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,23 +145,13 @@
|
|||
}
|
||||
|
||||
.stage-nav-item {
|
||||
display: flex;
|
||||
line-height: 65px;
|
||||
border-top: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-right: 1px solid $border-color;
|
||||
background-color: $gray-light;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
&.active {
|
||||
background-color: transparent;
|
||||
border-right-color: transparent;
|
||||
border-top-color: $border-color;
|
||||
border-bottom-color: $border-color;
|
||||
box-shadow: inset 2px 0 0 0 $blue-500;
|
||||
|
||||
.stage-name {
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
background: $blue-50;
|
||||
border-color: $blue-300;
|
||||
box-shadow: inset 4px 0 0 0 $blue-500;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
|
@ -178,24 +160,12 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top: 0;
|
||||
.stage-nav-item-cell.stage-name {
|
||||
width: 44.5%;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.stage-nav-item-cell {
|
||||
&.stage-median {
|
||||
margin-left: auto;
|
||||
margin-right: $gl-padding;
|
||||
min-width: calc(35% - #{$gl-padding});
|
||||
}
|
||||
}
|
||||
|
||||
.stage-name {
|
||||
padding-left: 16px;
|
||||
.stage-nav-item-cell.stage-median {
|
||||
min-width: 43%;
|
||||
}
|
||||
|
||||
.stage-empty,
|
||||
|
|
|
@ -34,40 +34,29 @@
|
|||
{{ n__('Last %d day', 'Last %d days', 90) }}
|
||||
.stage-panel-container
|
||||
.card.stage-panel
|
||||
.card-header
|
||||
.card-header.border-bottom-0
|
||||
%nav.col-headers
|
||||
%ul
|
||||
%li.stage-header
|
||||
%span.stage-name
|
||||
%li.stage-header.pl-5
|
||||
%span.stage-name.font-weight-bold
|
||||
{{ s__('ProjectLifecycle|Stage') }}
|
||||
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" }
|
||||
%li.median-header
|
||||
%span.stage-name
|
||||
%span.stage-name.font-weight-bold
|
||||
{{ __('Median') }}
|
||||
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" }
|
||||
%li.event-header
|
||||
%span.stage-name
|
||||
%li.event-header.pl-3
|
||||
%span.stage-name.font-weight-bold
|
||||
{{ currentStage ? __(currentStage.legend) : __('Related Issues') }}
|
||||
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" }
|
||||
%li.total-time-header
|
||||
%span.stage-name
|
||||
%li.total-time-header.pr-5.text-right
|
||||
%span.stage-name.font-weight-bold
|
||||
{{ __('Total Time') }}
|
||||
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" }
|
||||
.stage-panel-body
|
||||
%nav.stage-nav
|
||||
%ul
|
||||
%li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" }
|
||||
.stage-nav-item-cell.stage-name
|
||||
{{ stage.title }}
|
||||
.stage-nav-item-cell.stage-median
|
||||
%template{ "v-if" => "stage.isUserAllowed" }
|
||||
%span{ "v-if" => "stage.value" }
|
||||
{{ stage.value }}
|
||||
%span.stage-empty{ "v-else" => true }
|
||||
{{ __('Not enough data') }}
|
||||
%template{ "v-else" => true }
|
||||
%span.not-available
|
||||
{{ __('Not available') }}
|
||||
%stage-nav-item{ "v-for" => "stage in state.stages", ":key" => '`ca-stage-title-${stage.title}`', '@select' => 'selectStage(stage)', ":title" => "stage.title", ":is-user-allowed" => "stage.isUserAllowed", ":value" => "stage.value", ":is-active" => "stage.active" }
|
||||
.section.stage-events
|
||||
%template{ "v-if" => "isLoadingStage" }
|
||||
= icon("spinner spin")
|
||||
|
|
|
@ -4144,6 +4144,9 @@ msgstr ""
|
|||
msgid "Edit public deploy key"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit wiki page"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5658,6 +5661,9 @@ msgstr ""
|
|||
msgid "Hide shared projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hide stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hide value"
|
||||
msgid_plural "Hide values"
|
||||
msgstr[0] ""
|
||||
|
@ -9357,6 +9363,9 @@ msgstr ""
|
|||
msgid "Remove spent time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Remove time estimate"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import StageNavItem from '~/cycle_analytics/components/stage_nav_item.vue';
|
||||
|
||||
describe('StageNavItem', () => {
|
||||
let wrapper = null;
|
||||
const title = 'Cool stage';
|
||||
const value = '1 day';
|
||||
|
||||
function createComponent(props, shallow = true) {
|
||||
const func = shallow ? shallowMount : mount;
|
||||
return func(StageNavItem, {
|
||||
propsData: {
|
||||
canEdit: false,
|
||||
isActive: false,
|
||||
isUserAllowed: false,
|
||||
isDefaultStage: true,
|
||||
title,
|
||||
value,
|
||||
...props,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function hasStageName() {
|
||||
const stageName = wrapper.find('.stage-name');
|
||||
expect(stageName.exists()).toBe(true);
|
||||
expect(stageName.text()).toEqual(title);
|
||||
}
|
||||
|
||||
it('renders stage name', () => {
|
||||
wrapper = createComponent({ isUserAllowed: true });
|
||||
hasStageName();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('User has access', () => {
|
||||
describe('with a value', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ isUserAllowed: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
it('renders the value for median value', () => {
|
||||
expect(wrapper.find('.stage-empty').exists()).toBe(false);
|
||||
expect(wrapper.find('.not-available').exists()).toBe(false);
|
||||
expect(wrapper.find('.stage-median').text()).toEqual(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without a value', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ isUserAllowed: true, value: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('has the stage-empty class', () => {
|
||||
expect(wrapper.find('.stage-empty').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders Not enough data for the median value', () => {
|
||||
expect(wrapper.find('.stage-median').text()).toEqual('Not enough data');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('is active', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ isActive: true }, false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
it('has the active class', () => {
|
||||
expect(wrapper.find('.stage-nav-item').classes('active')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is not active', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
it('emits the `select` event when clicked', () => {
|
||||
expect(wrapper.emitted().select).toBeUndefined();
|
||||
wrapper.trigger('click');
|
||||
expect(wrapper.emitted().select.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User does not have access', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ isUserAllowed: false }, false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
it('renders stage name', () => {
|
||||
hasStageName();
|
||||
});
|
||||
|
||||
it('has class not-available', () => {
|
||||
expect(wrapper.find('.stage-empty').exists()).toBe(false);
|
||||
expect(wrapper.find('.not-available').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders Not available for the median value', () => {
|
||||
expect(wrapper.find('.stage-median').text()).toBe('Not available');
|
||||
});
|
||||
it('does not render options menu', () => {
|
||||
expect(wrapper.find('.more-actions-toggle').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('User can edit stages', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ canEdit: true, isUserAllowed: true }, false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
it('renders stage name', () => {
|
||||
hasStageName();
|
||||
});
|
||||
|
||||
it('renders options menu', () => {
|
||||
expect(wrapper.find('.more-actions-toggle').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('Default stages', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(
|
||||
{ canEdit: true, isUserAllowed: true, isDefaultStage: true },
|
||||
false,
|
||||
);
|
||||
});
|
||||
it('can hide the stage', () => {
|
||||
expect(wrapper.text()).toContain('Hide stage');
|
||||
});
|
||||
it('can not edit the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Edit stage');
|
||||
});
|
||||
it('can not remove the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Remove stage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom stages', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent(
|
||||
{ canEdit: true, isUserAllowed: true, isDefaultStage: false },
|
||||
false,
|
||||
);
|
||||
});
|
||||
it('can edit the stage', () => {
|
||||
expect(wrapper.text()).toContain('Edit stage');
|
||||
});
|
||||
it('can remove the stage', () => {
|
||||
expect(wrapper.text()).toContain('Remove stage');
|
||||
});
|
||||
|
||||
it('can not hide the stage', () => {
|
||||
expect(wrapper.text()).not.toContain('Hide stage');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue