diff options
author | Clement Ho <ClemMakesApps@gmail.com> | 2017-11-10 17:41:04 -0600 |
---|---|---|
committer | Clement Ho <ClemMakesApps@gmail.com> | 2017-11-10 17:41:04 -0600 |
commit | 74b87f02db2ebda0b2b16a60dd6759fe6e8de95a (patch) | |
tree | 7432d75b3f4fcd84f20fef7157164b900648cc0d /app | |
parent | e4df83d0252c57f84e795d0d50d9a82b07898f44 (diff) | |
download | gitlab-ce-74b87f02db2ebda0b2b16a60dd6759fe6e8de95a.tar.gz |
Backport of add-epic-sidebar
Diffstat (limited to 'app')
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; + } + } + } +} |