Commit 76cca713 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'add-epic-sidebar' into 'master'

Add epic sidebar

Closes #3556

See merge request gitlab-org/gitlab-ee!3253
parents ccb69ca0 b42b0f97
......@@ -135,7 +135,6 @@ window.dateFormat = dateFormat;
* @param {Number} seconds
* @return {String}
*/
// eslint-disable-next-line import/prefer-default-export
export function timeIntervalInWords(intervalInSeconds) {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
......@@ -149,3 +148,17 @@ export function timeIntervalInWords(intervalInSeconds) {
}
return text;
}
export function dateInWords(date, abbreviated = false) {
if (!date) return date;
const month = date.getMonth();
const year = date.getFullYear();
const monthNames = [s__('January'), s__('February'), s__('March'), s__('April'), s__('May'), s__('June'), s__('July'), s__('August'), s__('September'), s__('October'), s__('November'), s__('December')];
const monthNamesAbbr = [s__('Jan'), s__('Feb'), s__('Mar'), s__('Apr'), s__('May'), s__('Jun'), s__('Jul'), s__('Aug'), s__('Sep'), s__('Oct'), s__('Nov'), s__('Dec')];
const monthName = abbreviated ? monthNamesAbbr[month] : monthNames[month];
return `${monthName} ${date.getDate()}, ${year}`;
}
......@@ -24,6 +24,10 @@ export function highCountTrim(count) {
return count > 99 ? '99+' : count;
}
export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
......
<script>
import Pikaday from 'pikaday';
import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
export default {
name: 'datePicker',
props: {
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
},
methods: {
selected(dateText) {
this.$emit('newDateSelected', this.calendar.toString(dateText));
},
toggled() {
this.$emit('hidePicker');
},
},
mounted() {
this.calendar = new Pikaday({
field: this.$el.querySelector('.dropdown-menu-toggle'),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: this.$el,
defaultDate: this.selectedDate,
setDefaultDate: !!this.selectedDate,
minDate: this.minDate,
maxDate: this.maxDate,
parse: dateString => parsePikadayDate(dateString),
toString: date => pikadayToString(date),
onSelect: this.selected.bind(this),
onClose: this.toggled.bind(this),
});
this.$el.append(this.calendar.el);
this.calendar.show();
},
beforeDestroy() {
this.calendar.destroy();
},
};
</script>
<template>
<div class="pikaday-container">
<div class="dropdown open">
<button
type="button"
class="dropdown-menu-toggle"
data-toggle="dropdown"
@click="toggled"
>
<span class="dropdown-toggle-text">
{{label}}
</span>
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
</button>
</div>
</div>
</template>
<script>
export default {
name: 'collapsedCalendarIcon',
props: {
containerClass: {
type: String,
required: false,
default: '',
},
text: {
type: String,
required: false,
default: '',
},
showIcon: {
type: Boolean,
required: false,
default: true,
},
},
methods: {
click() {
this.$emit('click');
},
},
};
</script>
<template>
<div
:class="containerClass"
@click="click"
>
<i
v-if="showIcon"
class="fa fa-calendar"
aria-hidden="true"
>
</i>
<slot>
<span>
{{ text }}
</span>
</slot>
</div>
</template>
<script>
import { dateInWords } from '../../../lib/utils/datetime_utility';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
export default {
name: 'sidebarCollapsedGroupedDatePicker',
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
disableClickableIcons: {
type: Boolean,
required: false,
default: false,
},
},
components: {
toggleSidebar,
collapsedCalendarIcon,
},
computed: {
hasMinAndMaxDates() {
return this.minDate && this.maxDate;
},
hasNoMinAndMaxDates() {
return !this.minDate && !this.maxDate;
},
showMinDateBlock() {
return this.minDate || this.hasNoMinAndMaxDates;
},
showFromText() {
return !this.maxDate && this.minDate;
},
iconClass() {
const disabledClass = this.disableClickableIcons ? 'disabled' : '';
return `block sidebar-collapsed-icon calendar-icon ${disabledClass}`;
},
},
methods: {
toggleSidebar() {
this.$emit('toggleCollapse');
},
dateText(dateType = 'min') {
const date = this[`${dateType}Date`];
const dateWords = dateInWords(date, true);
const parsedDateWords = dateWords ? dateWords.replace(',', '') : dateWords;
return date ? parsedDateWords : 'None';
},
},
};
</script>
<template>
<div class="block sidebar-grouped-item">
<div
v-if="showToggleSidebar"
class="issuable-sidebar-header"
>
<toggle-sidebar
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
<collapsed-calendar-icon
v-if="showMinDateBlock"
:container-class="iconClass"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
<span v-if="showFromText">From</span>
<span>{{ dateText('min') }}</span>
</span>
</collapsed-calendar-icon>
<div
v-if="hasMinAndMaxDates"
class="text-center sidebar-collapsed-divider"
>
-
</div>
<collapsed-calendar-icon
v-if="maxDate"
:container-class="iconClass"
:show-icon="!minDate"
@click="toggleSidebar"
>
<span class="sidebar-collapsed-value">
<span v-if="!minDate">Until</span>
<span>{{ dateText('max') }}</span>
</span>
</collapsed-calendar-icon>
</div>
</template>
<script>
import datePicker from '../pikaday.vue';
import loadingIcon from '../loading_icon.vue';
import toggleSidebar from './toggle_sidebar.vue';
import collapsedCalendarIcon from './collapsed_calendar_icon.vue';
import { dateInWords } from '../../../lib/utils/datetime_utility';
export default {
name: 'sidebarDatePicker',
props: {
collapsed: {
type: Boolean,
required: false,
default: true,
},
showToggleSidebar: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
editable: {
type: Boolean,
required: false,
default: false,
},
label: {
type: String,
required: false,
default: 'Date picker',
},
selectedDate: {
type: Date,
required: false,
},
minDate: {
type: Date,
required: false,
},
maxDate: {
type: Date,
required: false,
},
},
data() {
return {
editing: false,
};
},
components: {
datePicker,
toggleSidebar,
loadingIcon,
collapsedCalendarIcon,
},
computed: {
selectedAndEditable() {
return this.selectedDate && this.editable;
},
selectedDateWords() {
return dateInWords(this.selectedDate, true);
},
collapsedText() {
return this.selectedDateWords ? this.selectedDateWords : 'None';
},
},
methods: {
stopEditing() {
this.editing = false;
},
toggleDatePicker() {
this.editing = !this.editing;
},
newDateSelected(date = null) {
this.date = date;
this.editing = false;
this.$emit('saveDate', date);
},
toggleSidebar() {
this.$emit('toggleCollapse');
},
},
};
</script>
<template>
<div class="block">
<div class="issuable-sidebar-header">
<toggle-sidebar
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
<collapsed-calendar-icon
class="sidebar-collapsed-icon"
:text="collapsedText"
/>
<div class="title">
{{ label }}
<loading-icon
v-if="isLoading"
:inline="true"
/>
<div class="pull-right">
<button
v-if="editable && !editing"
type="button"
class="btn-blank btn-link btn-primary-hover-link btn-sidebar-action"
@click="toggleDatePicker"
>
Edit
</button>
<toggle-sidebar
v-if="showToggleSidebar"
:collapsed="collapsed"
@toggle="toggleSidebar"
/>
</div>
</div>
<div class="value">
<date-picker
v-if="editing"
:selected-date="selectedDate"
:min-date="minDate"
:max-date="maxDate"
:label="label"
@newDateSelected="newDateSelected"
@hidePicker="stopEditing"
/>
<span
v-else
class="value-content"
>
<template v-if="selectedDate">
<strong>{{ selectedDateWords }}</strong>
<span
v-if="selectedAndEditable"
class="no-value"
>
-
<button
type="button"
class="btn-blank btn-link btn-secondary-hover-link"
@click="newDateSelected(null)"
>
remove
</button>
</span>
</template>
<span
v-else
class="no-value"
>
None
</span>
</span>
</div>
</div>
</template>
<script>
export default {
name: 'toggleSidebar',
props: {
collapsed: {
type: Boolean,
required: true,
},
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
</script>
<template>
<button
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle"
>
<i
aria-label="toggle collapse"
class="fa"
:class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }"
></i>
</button>
</template>
......@@ -412,6 +412,7 @@
padding: 0;
background: transparent;
border: 0;
border-radius: 0;
&:hover,
&:active,
......@@ -421,3 +422,25 @@
box-shadow: none;
}
}
.btn-link.btn-secondary-hover-link {
color: $gl-text-color-secondary;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
}
.btn-link.btn-primary-hover-link {
color: inherit;
&:hover,
&:active,
&:focus {
color: $gl-link-color;
text-decoration: none;
}
}
......@@ -43,11 +43,13 @@
}
.sidebar-collapsed-icon {
cursor: pointer;
.btn {
background-color: $gray-light;
}
&:not(.disabled) {
cursor: pointer;
}
}
}
......@@ -55,6 +57,10 @@
padding-right: 0;
z-index: 300;
.btn-sidebar-action {
display: inline-flex;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
......@@ -136,3 +142,18 @@
.issuable-sidebar {
@include new-style-dropdown;
}
.pikaday-container {
.pika-single {
margin-top: 2px;
width: 250px;
}
.dropdown-menu-toggle {
line-height: 20px;
}
}
.sidebar-collapsed-icon .sidebar-collapsed-value {
font-size: 12px;
}
......@@ -284,10 +284,15 @@
font-weight: $gl-font-weight-normal;
}
.no-value {
.no-value,
.btn-secondary-hover-link {
color: $gl-text-color-secondary;
}
.btn-secondary-hover-link:hover {
color: $gl-link-color;
}
.sidebar-collapsed-icon {
display: none;
}
......@@ -353,7 +358,8 @@
.gutter-toggle {
width: 100%;
margin-left: 0;
padding-left: 25px;
padding-left: 0;
text-align: center;
}
.sidebar-collapsed-icon {
......@@ -367,7 +373,7 @@
fill: $issuable-sidebar-color;
}
&:hover,
&:hover:not(.disabled),
&:hover .todo-undone {
color: $gl-text-color;
......@@ -953,3 +959,21 @@
.add-issuable-form-actions {
margin-top: $gl-padding;
}
.right-sidebar-collapsed {
.sidebar-grouped-item {
.sidebar-collapsed-icon {
margin-bottom: 0;
}
.sidebar-collapsed-divider {
line-height: 5px;
font-size: 12px;
color: $theme-gray-700;
+ .sidebar-collapsed-icon {
padding-top: 0;
}
}
}
}
......@@ -11,7 +11,8 @@ module NavHelper
if current_path?('merge_requests#show') ||
current_path?('projects/merge_requests/conflicts#show') ||
current_path?('issues#show') ||
current_path?('milestones#show')
current_path?('milestones#show') ||
current_path?('epics#show')
if cookies[:collapsed_gutter] == 'true'
%w[page-gutter right-sidebar-collapsed]
else
......
---
title: Add sidebar for epic
merge_request:
author:
type: added
<script>
import issuableApp from '~/issue_show/components/app.vue';
import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
export default {
name: 'epicShowApp',
......@@ -55,9 +56,18 @@
type: Object,
required: true,
},
startDate: {
type: String,
required: false,
},
endDate: {
type: String,
required: false,
},
},
components: {
epicHeader,
epicSidebar,
issuableApp,
},
created() {
......@@ -75,21 +85,29 @@
:author="author"
:created="created"
/>
<div class="issuable-details detail-page-description content-block">
<issuable-app
:can-update="canUpdate"
:can-destroy="canDestroy"
<div class="issuable-details content-block">
<div class="detail-page-description">
<issuable-app
:can-update="canUpdate"
:can-destroy="canDestroy"
:endpoint="endpoint"
:issuable-ref="issuableRef"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
/>
</div>
<epic-sidebar
:endpoint="endpoint"
:issuable-ref="issuableRef"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-inline-edit-button="true"
:editable="canUpdate"
:initialStartDate="startDate"
:initialEndDate="endDate"
/>
</div>
</div>
......
......@@ -12,6 +12,10 @@ document.addEventListener('DOMContentLoaded', () => {
canDestroy: false,
});
// Convert backend casing to match frontend style guide
props.startDate = props.start_date;
props.endDate = props.end_date;
return new Vue({
el,
components: {
......
<script>
import Cookies from 'js-cookie';
import Flash from '~/flash';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import sidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import SidebarService from '../services/sidebar_service';
import Store from '../stores/sidebar_store';
export default {
name: 'epicSidebar',
props: {
endpoint: {
type: String,
required: true,
},
editable: {
type: Boolean,
required: false,
default: false,
},
initialStartDate: {
type: String,
required: false,
},
initialEndDate: {
type: String,
required: false,
},
},
data() {
const store = new Store({
startDate: this.initialStartDate,
endDate: this.initialEndDate,
});
return {
store,
// Backend will pass the appropriate css class for the contentContainer
collapsed: Cookies.get('collapsed_gutter') === 'true',
savingStartDate: false,
savingEndDate: false,
service: new SidebarService(this.endpoint),
};
},
components: {
sidebarDatePicker,
sidebarCollapsedGroupedDatePicker,
},
methods: {
toggleSidebar() {
this.collapsed = !this.collapsed;
const contentContainer = this.$el.closest('.page-with-sidebar');
contentContainer.classList.toggle('right-sidebar-expanded');
contentContainer.classList.toggle('right-sidebar-collapsed');
Cookies.set('collapsed_gutter', this.collapsed);
},
saveDate(dateType = 'start', newDate) {
const type = dateType === 'start' ? dateType : 'end';
const capitalizedType = capitalizeFirstCharacter(type);
const serviceMethod = `update${capitalizedType}Date`;
const savingBoolean = `saving${capitalizedType}Date`;
this[savingBoolean] = true;
return this.service[serviceMethod](newDate)
.then(() => {
this[savingBoolean] = false;
this.store[`${type}Date`] = newDate;
})
.catch(() => {
this[savingBoolean] = false;
Flash(`An error occurred while saving ${type} date`);
});
},
saveStartDate(date) {
return this.saveDate('start', date);
},
saveEndDate(date) {
return this.saveDate('end', date);
},
},
};
</script>
<template>
<aside
class="right-sidebar"
:class="{ 'right-sidebar-expanded' : !collapsed, 'right-sidebar-collapsed': collapsed }"
>
<div class="issuable-sidebar">
<sidebar-date-picker
v-if="!collapsed"
:collapsed="collapsed"
:is-loading="savingStartDate"
:editable="editable"
label="Planned start date"
:selected-date="store.startDateTime"
:max-date="store.endDateTime"
:show-toggle-sidebar="true"
@saveDate="saveStartDate"
@toggleCollapse="toggleSidebar"
/>
<sidebar-date-picker
v-if="!collapsed"
:collapsed="collapsed"
:is-loading="savingEndDate"
:editable="editable"
label="Planned finish date"
:selected-date="store.endDateTime"
:min-date="store.startDateTime"
@saveDate="saveEndDate"
@toggleCollapse="toggleSidebar"
/>
<sidebar-collapsed-grouped-date-picker
v-if="collapsed"
:collapsed="collapsed"
:min-date="store.startDateTime"
:max-date="store.endDateTime"
:show-toggle-sidebar="true"
@toggleCollapse="toggleSidebar"
/>
</div>
</aside>
</template>
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class SidebarService {
constructor(endpoint) {
this.endpoint = endpoint;
this.resource = Vue.resource(`${this.endpoint}.json`, {});
}
updateStartDate(startDate) {
return this.resource.update({
start_date: startDate,
});
}
updateEndDate(endDate) {
return this.resource.update({
end_date: endDate,
});
}
}
import { parsePikadayDate } from '~/lib/utils/datefix';
export default class SidebarStore {
constructor({ startDate, endDate }) {
this.startDate = startDate;
this.endDate = endDate;
}
get startDateTime() {
return this.startDate ? parsePikadayDate(this.startDate) : null;
}
get endDateTime() {
return this.endDate ? parsePikadayDate(this.endDate) : null;
}
}
......@@ -9,7 +9,9 @@ module EpicsHelper
url: user_path(author),
username: "@#{author.username}",
src: avatar_icon(@epic.author)
}
},
start_date: @epic.start_date,
end_date: @epic.end_date
}
data.to_json
......
......@@ -8,7 +8,7 @@ describe EpicsHelper do
user = create(:user)
@epic = create(:epic, author: user)
expect(JSON.parse(epic_meta_data).keys).to match_array(%w[created author])
expect(JSON.parse(epic_meta_data).keys).to match_array(%w[created author start_date end_date])
expect(JSON.parse(epic_meta_data)['author']).to eq({
'name' => user.name,
'url' => "/#{user.username}",
......
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import * as datetimeUtility from '~/lib/utils/datetime_utility';
(() => {
describe('Date time utils', () => {
......@@ -89,10 +89,22 @@ import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
describe('timeIntervalInWords', () => {
it('should return string with number of minutes and seconds', () => {
expect(timeIntervalInWords(9.54)).toEqual('9 seconds');
expect(timeIntervalInWords(1)).toEqual('1 second');
expect(timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
expect(timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds');
expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second');
expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
});
});
describe('dateInWords', () => {
const date = new Date('07/01/2016');
it('should return date in words', () => {
expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016');
});
it('should return abbreviated month name', () => {
expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016');
});
});
})();
import Vue from 'vue';
import epicShowApp from 'ee/epics/epic_show/components/epic_show_app.vue';
import epicHeader from 'ee/epics/epic_show/components/epic_header.vue';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import issuableApp from '~/issue_show/components/app.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import { props } from '../mock_data';
......@@ -10,6 +11,7 @@ describe('EpicShowApp', () => {
let vm;
let headerVm;
let issuableAppVm;
let sidebarVm;
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify(issueShowData.initialRequest), {
......@@ -26,6 +28,8 @@ describe('EpicShowApp', () => {
endpoint,
initialTitleHtml,
initialTitleText,
startDate,
endDate,
markdownPreviewPath,
markdownDocsPath,
author,
......@@ -57,6 +61,14 @@ describe('EpicShowApp', () => {
projectNamespace: '',
showInlineEditButton: true,
});
const EpicSidebar = Vue.extend(epicSidebar);
sidebarVm = mountComponent(EpicSidebar, {
endpoint,
editable: canUpdate,
initialStartDate: startDate,
initialEndDate: endDate,
});
});
afterEach(() => {
......@@ -70,4 +82,8 @@ describe('EpicShowApp', () => {
it('should render issuable-app', () => {
expect(vm.$el.innerHTML.indexOf(issuableAppVm.$el.innerHTML) !== -1).toEqual(true);
});
it('should render epic-sidebar', () => {
expect(vm.$el.innerHTML.indexOf(sidebarVm.$el.innerHTML) !== -1).toEqual(true);
});
});
......@@ -7,6 +7,8 @@ export const contentProps = {
groupPath: '',
initialTitleHtml: '',
initialTitleText: '',
startDate: '2017-01-01',
endDate: '2017-10-10',
};
export const headerProps = {
......
import SidebarStore from 'ee/epics/sidebar/stores/sidebar_store';
describe('Sidebar Store', () => {
const dateString = '2017-01-20';
describe('constructor', () => {
it('should set startDate', () => {
const store = new SidebarStore({
startDate: dateString,
});
expect(store.startDate).toEqual(dateString);
});
it('should set endDate', () => {
const store = new SidebarStore({
endDate: dateString,
});
expect(store.endDate).toEqual(dateString);
});
});
describe('startDateTime', () => {
it('should return null when there is no startDate', () => {
const store = new SidebarStore({});
expect(store.startDateTime).toEqual(null);
});
it('should return date', () => {
const store = new SidebarStore({
startDate: dateString,
});
const date = store.startDateTime;
expect(date.getDate()).toEqual(20);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2017);
});
});
describe('endDateTime', () => {
it('should return null when there is no endDate', () => {
const store = new SidebarStore({});
expect(store.endDateTime).toEqual(null);
});
it('should return date', () => {
const store = new SidebarStore({
endDate: dateString,
});
const date = store.endDateTime;
expect(date.getDate()).toEqual(20);
expect(date.getMonth()).toEqual(0);
expect(date.getFullYear()).toEqual(2017);
});
});
});
import { highCountTrim } from '~/lib/utils/text_utility';
import * as textUtility from '~/lib/utils/text_utility';
describe('text_utility', () => {
describe('gl.text.getTextWidth', () => {
......@@ -37,12 +37,18 @@ describe('text_utility', () => {
describe('highCountTrim', () => {
it('returns 99+ for count >= 100', () => {
expect(highCountTrim(105)).toBe('99+');
expect(highCountTrim(100)).toBe('99+');
expect(textUtility.highCountTrim(105)).toBe('99+');
expect(textUtility.highCountTrim(100)).toBe('99+');
});
it('returns exact number for count < 100', () => {
expect(highCountTrim(45)).toBe(45);
expect(textUtility.highCountTrim(45)).toBe(45);
});
});
describe('capitalizeFirstCharacter', () => {
it('returns string with first letter capitalized', () => {
expect(textUtility.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
});
});
......
import Vue from 'vue';
import datePicker from '~/vue_shared/components/pikaday.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('datePicker', () => {
let vm;
beforeEach(() => {
const DatePicker = Vue.extend(datePicker);
vm = mountComponent(DatePicker, {
label: 'label',
});
});
it('should render label text', () => {
expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label');
});
it('should show calendar', () => {
expect(vm.$el.querySelector('.pika-single')).toBeDefined();
});
it('should toggle when dropdown is clicked', () => {
const hidePicker = jasmine.createSpy();
vm.$on('hidePicker', hidePicker);
vm.$el.querySelector('.dropdown-menu-toggle').click();
expect(hidePicker).toHaveBeenCalled();
});
});
import Vue from 'vue';
import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('collapsedCalendarIcon', () => {
let vm;
beforeEach(() => {
const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon);
vm = mountComponent(CollapsedCalendarIcon, {
containerClass: 'test-class',
text: 'text',
showIcon: false,
});
});
it('should add class to container', () => {
expect(vm.$el.classList.contains('test-class')).toEqual(true);
});
it('should hide calendar icon if showIcon', () => {
expect(vm.$el.querySelector('.fa-calendar')).toBeNull();
});
it('should render text', () => {
expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text');
});
it('should emit click event when container is clicked', () => {
const click = jasmine.createSpy();
vm.$on('click', click);
vm.$el.click();
expect(click).toHaveBeenCalled();
});
});
import Vue from 'vue';
import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('collapsedGroupedDatePicker', () => {
let vm;
beforeEach(() => {
const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker);
vm = mountComponent(CollapsedGroupedDatePicker, {
showToggleSidebar: true,
});
});
it('should render toggle sidebar if showToggleSidebar', (done) => {
expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined();
vm.showToggleSidebar = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull();
done();
});
});
it('toggleCollapse events', () => {
const toggleCollapse = jasmine.createSpy();
beforeEach((done) => {
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
it('should emit when sidebar is toggled', () => {
vm.$el.querySelector('.gutter-toggle').click();
expect(toggleCollapse).toHaveBeenCalled();
});
it('should emit when collapsed-calendar-icon is clicked', () => {
vm.$el.querySelector('.sidebar-collapsed-icon').click();
expect(toggleCollapse).toHaveBeenCalled();
});
});
describe('minDate and maxDate', () => {
beforeEach((done) => {
vm.minDate = new Date('07/17/2016');
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
});
it('should render both collapsed-calendar-icon', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons.length).toEqual(2);
expect(icons[0].innerText.trim()).toEqual('Jul 17 2016');
expect(icons[1].innerText.trim()).toEqual('Jul 17 2017');
});
});
describe('minDate', () => {
beforeEach((done) => {
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
it('should render minDate in collapsed-calendar-icon', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016');
});
});
describe('maxDate', () => {
beforeEach((done) => {
vm.maxDate = new Date('07/17/2017');
Vue.nextTick(done);
});
it('should render maxDate in collapsed-calendar-icon', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017');
});
});
describe('no dates', () => {
it('should render None', () => {
const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon');
expect(icons.length).toEqual(1);
expect(icons[0].innerText.trim()).toEqual('None');
});
});
});
import Vue from 'vue';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('sidebarDatePicker', () => {
let vm;
beforeEach(() => {
const SidebarDatePicker = Vue.extend(sidebarDatePicker);
vm = mountComponent(SidebarDatePicker, {
label: 'label',
isLoading: true,
});
});
it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
const toggleCollapse = jasmine.createSpy();
vm.$on('toggleCollapse', toggleCollapse);
vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click();
expect(toggleCollapse).toHaveBeenCalled();
});
it('should render collapsed-calendar-icon', () => {
expect(vm.$el.querySelector('.sidebar-collapsed-icon')).toBeDefined();
});
it('should render label', () => {
expect(vm.$el.querySelector('.title').innerText.trim()).toEqual('label');
});
it('should render loading-icon when isLoading', () => {
expect(vm.$el.querySelector('.fa-spin')).toBeDefined();
});
it('should render value when not editing', () => {
expect(vm.$el.querySelector('.value-content')).toBeDefined();
});
it('should render None if there is no selectedDate', () => {
expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None');
});
it('should render date-picker when editing', (done) => {
vm.editing = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.pika-label')).toBeDefined();
done();
});
});
describe('editable', () => {
beforeEach((done) => {
vm.editable = true;
Vue.nextTick(done);
});
it('should render edit button', () => {
expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit');
});
it('should enable editing when edit button is clicked', (done) => {
vm.isLoading = false;
Vue.nextTick(() => {
vm.$el.querySelector('.title .btn-blank').click();
expect(vm.editing).toEqual(true);
done();
});
});
});
it('should render date if selectedDate', (done) => {
vm.selectedDate = new Date('07/07/2017');
Vue.nextTick(() => {
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017');
done();
});
});
describe('selectedDate and editable', () => {
beforeEach((done) => {
vm.selectedDate = new Date('07/07/2017');
vm.editable = true;
Vue.nextTick(done);
});
it('should render remove button if selectedDate and editable', () => {
expect(vm.$el.querySelector('.value-content .btn-blank').innerText.trim()).toEqual('remove');
});
it('should emit saveDate when remove button is clicked', () => {
const saveDate = jasmine.createSpy();
vm.$on('saveDate', saveDate);
vm.$el.querySelector('.value-content .btn-blank').click();
expect(saveDate).toHaveBeenCalled();
});
});
describe('showToggleSidebar', () => {
beforeEach((done) => {
vm.showToggleSidebar = true;
Vue.nextTick(done);
});
it('should render toggle-sidebar when showToggleSidebar', () => {
expect(vm.$el.querySelector('.title .gutter-toggle')).toBeDefined();
});
it('should emit toggleCollapse when toggle sidebar is clicked', () => {
const toggleCollapse = jasmine.createSpy();
vm.$on('toggleCollapse', toggleCollapse);
vm.$el.querySelector('.title .gutter-toggle').click();
expect(toggleCollapse).toHaveBeenCalled();
});
});
});
import Vue from 'vue';
import Cookies from 'js-cookie';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('epicSidebar', () => {
let vm;
let originalCookieState;
let EpicSidebar;
beforeEach(() => {
setFixtures(`
<div class="page-with-sidebar right-sidebar-expanded">
<div id="epic-sidebar"></div>
</div>
`);
originalCookieState = Cookies.get('collapsed_gutter');
Cookies.set('collapsed_gutter', null);
EpicSidebar = Vue.extend(epicSidebar);
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
}, '#epic-sidebar');
});
afterEach(() => {
Cookies.set('collapsed_gutter', originalCookieState);
});
it('should render right-sidebar-expanded class when not collapsed', () => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toEqual(true);
});
it('should render min date sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
});
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017');
});
it('should render max date sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialEndDate: '2018-01-01',
});
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018');
});
it('should render both sidebar-date-picker', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
initialEndDate: '2018-01-01',
});
const datePickers = vm.$el.querySelectorAll('.block');
expect(datePickers[0].querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017');
expect(datePickers[1].querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018');
});
describe('when collapsed', () => {
beforeEach(() => {
Cookies.set('collapsed_gutter', 'true');
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
});
});
it('should render right-sidebar-collapsed class', () => {
expect(vm.$el.classList.contains('right-sidebar-collapsed')).toEqual(true);
});
it('should render collapsed grouped date picker', () => {
expect(vm.$el.querySelector('.sidebar-collapsed-icon span').innerText.trim()).toEqual('From Jan 1 2017');
});
});
describe('toggleSidebar', () => {
it('should toggle collapsed_gutter cookie', () => {
expect(vm.$el.classList.contains('right-sidebar-expanded')).toEqual(true);
vm.$el.querySelector('.gutter-toggle').click();
expect(Cookies.get('collapsed_gutter')).toEqual('true');
});
it('should toggle contentContainer css class', () => {
const contentContainer = document.querySelector('.page-with-sidebar');
expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(true);
expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(false);
vm.$el.querySelector('.gutter-toggle').click();
expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(false);
expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(true);
});
});
describe('saveDate', () => {
let interceptor;
let component;
beforeEach(() => {
interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 200,
}));
};
Vue.http.interceptors.push(interceptor);
component = new EpicSidebar({
propsData: {
endpoint: gl.TEST_HOST,
},
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should save startDate', (done) => {
const date = '2017-01-01';
expect(component.store.startDate).toBeUndefined();
component.saveStartDate(date)
.then(() => {
expect(component.store.startDate).toEqual(date);
done();
})
.catch(done.fail);
});
it('should save endDate', (done) => {
const date = '2017-01-01';
expect(component.store.endDate).toBeUndefined();
component.saveEndDate(date)
.then(() => {
expect(component.store.endDate).toEqual(date);
done();
})
.catch(done.fail);
});
it('should handle errors gracefully', () => {});
});
describe('saveDate error', () => {
let interceptor;
let component;
beforeEach(() => {
interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 500,
}));
};
Vue.http.interceptors.push(interceptor);
component = new EpicSidebar({
propsData: {
endpoint: gl.TEST_HOST,
},
});
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
it('should handle errors gracefully', (done) => {
const date = '2017-01-01';
expect(component.store.startDate).toBeUndefined();
component.saveDate('start', date)
.then(() => {
expect(component.store.startDate).toBeUndefined();
done();
})
.catch(done.fail);
});
});
});
import Vue from 'vue';
import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('toggleSidebar', () => {
let vm;
beforeEach(() => {
const ToggleSidebar = Vue.extend(toggleSidebar);
vm = mountComponent(ToggleSidebar, {
collapsed: true,
});
});
it('should render << when collapsed', () => {
expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true);
});
it('should render >> when collapsed', () => {
vm.collapsed = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true);
});
});
it('should emit toggle event when button clicked', () => {
const toggle = jasmine.createSpy();
vm.$on('toggle', toggle);
vm.$el.click();
expect(toggle).toHaveBeenCalled();
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment