summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorClement Ho <ClemMakesApps@gmail.com>2017-11-10 17:41:04 -0600
committerClement Ho <ClemMakesApps@gmail.com>2017-11-10 17:41:04 -0600
commit74b87f02db2ebda0b2b16a60dd6759fe6e8de95a (patch)
tree7432d75b3f4fcd84f20fef7157164b900648cc0d /app
parente4df83d0252c57f84e795d0d50d9a82b07898f44 (diff)
downloadgitlab-ce-74b87f02db2ebda0b2b16a60dd6759fe6e8de95a.tar.gz
Backport of add-epic-sidebar
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js15
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue79
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue109
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue163
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue30
-rw-r--r--app/assets/stylesheets/framework/buttons.scss23
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss25
-rw-r--r--app/assets/stylesheets/pages/issuable.scss29
10 files changed, 518 insertions, 5 deletions
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 29fc91733b3..8d0bab55843 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -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}`;
+}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index f776829f69c..0decfff6921 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -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);
};
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
new file mode 100644
index 00000000000..d8d974a2ff7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -0,0 +1,79 @@
+<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>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
new file mode 100644
index 00000000000..a88e1310131
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon.vue
@@ -0,0 +1,46 @@
+<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>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
new file mode 100644
index 00000000000..9ede5553bc5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue
@@ -0,0 +1,109 @@
+<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>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
new file mode 100644
index 00000000000..9c3413377a3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -0,0 +1,163 @@
+<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>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
new file mode 100644
index 00000000000..5ae76adad71
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
@@ -0,0 +1,30 @@
+<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>
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index b2f26cf7159..dfa3d4c6fb9 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -408,6 +408,7 @@
padding: 0;
background: transparent;
border: 0;
+ border-radius: 0;
&:hover,
&:active,
@@ -417,3 +418,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;
+ }
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 1a19b7320a0..792981fdc48 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -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;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 760c7c80aff..d9d00197dc0 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -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;
}
@@ -295,6 +300,8 @@
.gutter-toggle {
margin-top: 7px;
border-left: 1px solid $border-gray-normal;
+ padding-left: 0;
+ text-align: center;
}
.title .gutter-toggle {
@@ -367,7 +374,7 @@
fill: $issuable-sidebar-color;
}
- &:hover,
+ &:hover:not(.disabled),
&:hover .todo-undone {
color: $gl-text-color;
@@ -908,3 +915,21 @@
margin: 0 3px;
}
}
+
+.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;
+ }
+ }
+ }
+}