Commit 76cca713 by Filipa Lacerda

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

Add epic sidebar

Closes #3556

See merge request !3253
parents ccb69ca0 b42b0f97
Pipeline #13815016 failed with stages
in 112 minutes 30 seconds
......@@ -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,