Commit 6c868a01 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '25010-collapsed-sidebar-tooltips' into 'master'

Resolve "Improve tooltips of collapsed sidebars"

Closes #25010 et #44272

See merge request gitlab-org/gitlab-ce!17714
parents 79905b5a 49b85262
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
import $ from 'jquery'; import $ from 'jquery';
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
class DueDateSelect { class DueDateSelect {
...@@ -14,6 +16,7 @@ class DueDateSelect { ...@@ -14,6 +16,7 @@ class DueDateSelect {
this.$dropdownParent = $dropdownParent; this.$dropdownParent = $dropdownParent;
this.$datePicker = $dropdownParent.find('.js-due-date-calendar'); this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
this.$block = $block; this.$block = $block;
this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
this.$selectbox = $dropdown.closest('.selectbox'); this.$selectbox = $dropdown.closest('.selectbox');
this.$value = $block.find('.value'); this.$value = $block.find('.value');
this.$valueContent = $block.find('.value-content'); this.$valueContent = $block.find('.value-content');
...@@ -128,7 +131,8 @@ class DueDateSelect { ...@@ -128,7 +131,8 @@ class DueDateSelect {
submitSelectedDate(isDropdown) { submitSelectedDate(isDropdown) {
const selectedDateValue = this.datePayload[this.abilityName].due_date; const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; const hasDueDate = this.displayedDate !== 'No due date';
const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
this.$loading.removeClass('hidden').fadeIn(); this.$loading.removeClass('hidden').fadeIn();
...@@ -145,10 +149,13 @@ class DueDateSelect { ...@@ -145,10 +149,13 @@ class DueDateSelect {
return axios.put(this.issueUpdateURL, this.datePayload) return axios.put(this.issueUpdateURL, this.datePayload)
.then(() => { .then(() => {
const tooltipText = hasDueDate ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})` : __('Due date');
if (isDropdown) { if (isDropdown) {
this.$dropdown.trigger('loaded.gl.dropdown'); this.$dropdown.trigger('loaded.gl.dropdown');
this.$dropdown.dropdown('toggle'); this.$dropdown.dropdown('toggle');
} }
this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
return this.$loading.fadeOut(); return this.$loading.fadeOut();
}); });
} }
......
...@@ -83,7 +83,7 @@ export default class LabelsSelect { ...@@ -83,7 +83,7 @@ export default class LabelsSelect {
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
axios.put(issueUpdateURL, data) axios.put(issueUpdateURL, data)
.then(({ data }) => { .then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles; var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
$loading.fadeOut(); $loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide(); $selectbox.hide();
...@@ -115,8 +115,7 @@ export default class LabelsSelect { ...@@ -115,8 +115,7 @@ export default class LabelsSelect {
labelTooltipTitle = labelTitles.join(', '); labelTooltipTitle = labelTitles.join(', ');
} }
else { else {
labelTooltipTitle = ''; labelTooltipTitle = __('Labels');
$sidebarLabelTooltip.tooltip('destroy');
} }
$sidebarLabelTooltip $sidebarLabelTooltip
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
...@@ -25,7 +26,7 @@ export default class MilestoneSelect { ...@@ -25,7 +26,7 @@ export default class MilestoneSelect {
} }
$els.each((i, dropdown) => { $els.each((i, dropdown) => {
let collapsedSidebarLabelTemplate, milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault; let milestoneLinkNoneTemplate, milestoneLinkTemplate, selectedMilestone, selectedMilestoneDefault;
const $dropdown = $(dropdown); const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId'); const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones'); const milestonesUrl = $dropdown.data('milestones');
...@@ -52,7 +53,6 @@ export default class MilestoneSelect { ...@@ -52,7 +53,6 @@ export default class MilestoneSelect {
if (issueUpdateURL) { if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>');
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
...@@ -214,10 +214,16 @@ export default class MilestoneSelect { ...@@ -214,10 +214,16 @@ export default class MilestoneSelect {
data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title; data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); return $sidebarCollapsedValue
.attr('data-original-title', `${data.milestone.name}<br />${data.milestone.remaining}`)
.find('span')
.text(data.milestone.title);
} else { } else {
$value.html(milestoneLinkNoneTemplate); $value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No'); return $sidebarCollapsedValue
.attr('data-original-title', __('Milestone'))
.find('span')
.text(__('None'));
} }
}) })
.catch(() => { .catch(() => {
......
...@@ -5,6 +5,7 @@ import _ from 'underscore'; ...@@ -5,6 +5,7 @@ import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { __ } from './locale';
function Sidebar(currentUser) { function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this); this.toggleTodo = this.toggleTodo.bind(this);
...@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() { ...@@ -41,12 +42,14 @@ Sidebar.prototype.addEventListeners = function() {
}; };
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, isExpanded, tooltipLabel;
e.preventDefault(); e.preventDefault();
$this = $(this); $this = $(this);
$thisIcon = $this.find('i'); isExpanded = $this.find('i').hasClass('fa-angle-double-right');
tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
$allGutterToggleIcons = $('.js-sidebar-toggle i'); $allGutterToggleIcons = $('.js-sidebar-toggle i');
if ($thisIcon.hasClass('fa-angle-double-right')) {
if (isExpanded) {
$allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
$('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
$('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
...@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { ...@@ -57,6 +60,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
if (gl.lazyLoader) gl.lazyLoader.loadCheck(); if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} }
$this.attr('data-original-title', tooltipLabel);
if (!triggered) { if (!triggered) {
Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
} }
......
<script> <script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'Assignees', name: 'Assignees',
directives: {
tooltip,
},
props: { props: {
rootPath: { rootPath: {
type: String, type: String,
...@@ -14,6 +20,11 @@ export default { ...@@ -14,6 +20,11 @@ export default {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
issuableType: {
type: String,
require: true,
default: 'issue',
},
}, },
data() { data() {
return { return {
...@@ -62,6 +73,12 @@ export default { ...@@ -62,6 +73,12 @@ export default {
names.push(`+ ${this.users.length - maxRender} more`); names.push(`+ ${this.users.length - maxRender} more`);
} }
if (!this.users.length) {
const emptyTooltipLabel = this.issuableType === 'issue' ?
__('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel);
}
return names.join(', '); return names.join(', ');
}, },
sidebarAvatarCounter() { sidebarAvatarCounter() {
...@@ -109,7 +126,8 @@ export default { ...@@ -109,7 +126,8 @@ export default {
<div> <div>
<div <div
class="sidebar-collapsed-icon sidebar-collapsed-user" class="sidebar-collapsed-icon sidebar-collapsed-user"
:class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }" :class="{ 'multiple-users': hasMoreThanOneAssignee }"
v-tooltip
data-container="body" data-container="body"
data-placement="left" data-placement="left"
:title="collapsedTooltipTitle" :title="collapsedTooltipTitle"
......
<script> <script>
import Flash from '../../../flash'; import Flash from '~/flash';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue'; import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue'; import Assignees from './assignees.vue';
import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
export default { export default {
name: 'SidebarAssignees', name: 'SidebarAssignees',
...@@ -25,6 +25,11 @@ export default { ...@@ -25,6 +25,11 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
issuableType: {
type: String,
require: true,
default: 'issue',
},
}, },
data() { data() {
return { return {
...@@ -90,6 +95,7 @@ export default { ...@@ -90,6 +95,7 @@ export default {
:users="store.assignees" :users="store.assignees"
:editable="store.editable" :editable="store.editable"
@assign-self="assignSelf" @assign-self="assignSelf"
:issuable-type="issuableType"
/> />
</div> </div>
</template> </template>
<script> <script>
import Flash from '../../../flash'; import { __ } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import { __ } from '../../../locale';
import eventHub from '../../event_hub';
export default { export default {
components: { components: {
editForm, editForm,
Icon, Icon,
}, },
directives: {
tooltip,
},
props: { props: {
isConfidential: { isConfidential: {
required: true, required: true,
...@@ -33,6 +37,9 @@ export default { ...@@ -33,6 +37,9 @@ export default {
confidentialityIcon() { confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye'; return this.isConfidential ? 'eye-slash' : 'eye';
}, },
tooltipLabel() {
return this.isConfidential ? __('Confidential') : __('Not confidential');
},
}, },
created() { created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm); eventHub.$on('closeConfidentialityForm', this.toggleForm);
...@@ -65,6 +72,10 @@ export default { ...@@ -65,6 +72,10 @@ export default {
<div <div
class="sidebar-collapsed-icon" class="sidebar-collapsed-icon"
@click="toggleForm" @click="toggleForm"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
> >
<icon <icon
:name="confidentialityIcon" :name="confidentialityIcon"
......
<script> <script>
import { __ } from '~/locale';
import Flash from '~/flash'; import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue';
import eventHub from '../../event_hub';
export default { export default {
components: { components: {
editForm, editForm,
Icon, Icon,
}, },
directives: {
tooltip,
},
mixins: [issuableMixin], mixins: [issuableMixin],
props: { props: {
...@@ -44,6 +51,10 @@ export default { ...@@ -44,6 +51,10 @@ export default {
isLockDialogOpen() { isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen; return this.mediator.store.isLockDialogOpen;
}, },
tooltipLabel() {
return this.isLocked ? __('Locked') : __('Unlocked');
},
}, },
created() { created() {
...@@ -85,6 +96,10 @@ export default { ...@@ -85,6 +96,10 @@ export default {
<div <div
class="sidebar-collapsed-icon" class="sidebar-collapsed-icon"
@click="toggleForm" @click="toggleForm"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
> >
<icon <icon
:name="lockIcon" :name="lockIcon"
......
<script> <script>
import { __, n__, sprintf } from '../../../locale'; import { __, n__, sprintf } from '~/locale';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import tooltip from '~/vue_shared/directives/tooltip';
import userAvatarImage from '../../../vue_shared/components/user_avatar/user_avatar_image.vue'; import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
export default { export default {
directives: {
tooltip,
},
components: { components: {
loadingIcon, loadingIcon,
userAvatarImage, userAvatarImage,
...@@ -72,7 +76,13 @@ ...@@ -72,7 +76,13 @@
<template> <template>
<div> <div>
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
v-tooltip
data-container="body"
data-placement="left"
:title="participantLabel"
>
<i <i
class="fa fa-users" class="fa fa-users"
aria-hidden="true" aria-hidden="true"
......
<script> <script>
import icon from '../../../vue_shared/components/icon.vue'; import { __, sprintf } from '~/locale';
import { abbreviateTime } from '../../../lib/utils/pretty_time'; import { abbreviateTime } from '~/lib/utils/pretty_time';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
name: 'TimeTrackingCollapsedState', name: 'TimeTrackingCollapsedState',
components: { components: {
icon, icon,
}, },
directives: {
tooltip,
},
props: { props: {
showComparisonState: { showComparisonState: {
type: Boolean, type: Boolean,
...@@ -79,6 +84,21 @@ ...@@ -79,6 +84,21 @@
return ''; return '';
}, },
timeTrackedTooltipText() {
let title;
if (this.showComparisonState) {
title = __('Time remaining');
} else if (this.showEstimateOnlyState) {
title = __('Estimated');
} else if (this.showSpentOnlyState) {
title = __('Time spent');
}
return sprintf('%{title}: %{text}', ({ title, text: this.text }));
},
tooltipText() {
return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
}, },
methods: { methods: {
abbreviateTime(timeStr) { abbreviateTime(timeStr) {
...@@ -89,7 +109,13 @@ ...@@ -89,7 +109,13 @@
</script> </script>
<template> <template>
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipText"
>
<icon name="timer" /> <icon name="timer" />
<div class="time-tracking-collapsed-summary"> <div class="time-tracking-collapsed-summary">
<div :class="divClass"> <div :class="divClass">
......
...@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) { ...@@ -27,6 +27,7 @@ function mountAssigneesComponent(mediator) {
mediator, mediator,
field: el.dataset.field, field: el.dataset.field,
signedIn: el.hasAttribute('data-signed-in'), signedIn: el.hasAttribute('data-signed-in'),
issuableType: gl.utils.isInIssuePage() ? 'issue' : 'merge_request',
}, },
}), }),
}); });
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
...@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -182,7 +183,7 @@ function UsersSelect(currentUser, els, options = {}) {
return axios.put(issueURL, data) return axios.put(issueURL, data)
.then(({ data }) => { .then(({ data }) => {
var user; var user, tooltipTitle;
$dropdown.trigger('loaded.gl.dropdown'); $dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut(); $loading.fadeOut();
if (data.assignee) { if (data.assignee) {
...@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -191,15 +192,17 @@ function UsersSelect(currentUser, els, options = {}) {
username: data.assignee.username, username: data.assignee.username,
avatar: data.assignee.avatar_url avatar: data.assignee.avatar_url
}; };
tooltipTitle = _.escape(user.name);
} else { } else {
user = { user = {
name: 'Unassigned', name: 'Unassigned',
username: '', username: '',
avatar: '' avatar: ''
}; };
tooltipTitle = __('Assignee');
} }
$value.html(assigneeTemplate(user)); $value.html(assigneeTemplate(user));
$collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle'); $collapsedSidebar.attr('title', tooltipTitle).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
}); });
}; };
......
<script> <script>
export default { import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'ToggleSidebar', name: 'ToggleSidebar',
directives: {
tooltip,
},
props: { props: {
collapsed: { collapsed: {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
}, },
computed: {
tooltipLabel() {
return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
},
methods: { methods: {
toggle() { toggle() {
this.$emit('toggle'); this.$emit('toggle');
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -20,6 +31,10 @@ ...@@ -20,6 +31,10 @@
type="button" type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action" class="btn btn-blank gutter-toggle btn-sidebar-action"
@click="toggle" @click="toggle"
v-tooltip
data-container="body"
data-placement="left"
:title="tooltipLabel"
> >
<i <i
aria-label="toggle collapse" aria-label="toggle collapse"
......
...@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px; ...@@ -247,6 +247,7 @@ $btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px; $btn-xs-side-margin: 5px;
$issue-status-expired: $orange-500; $issue-status-expired: $orange-500;
$issuable-sidebar-color: $gl-text-color-secondary; $issuable-sidebar-color: $gl-text-color-secondary;
$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999; $group-path-color: #999;
$namespace-kind-color: #aaa; $namespace-kind-color: #aaa;
$panel-heading-link-color: #777; $panel-heading-link-color: #777;
...@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400; ...@@ -373,6 +374,8 @@ $dropdown-hover-color: $blue-400;
$link-active-background: rgba(0, 0, 0, 0.04); $link-active-background: rgba(0, 0, 0, 0.04);
$link-hover-background: rgba(0, 0, 0, 0.06); $link-hover-background: rgba(0, 0, 0, 0.06);
$inactive-badge-background: rgba(0, 0, 0, 0.08); $inactive-badge-background: rgba(0, 0, 0, 0.08);
$sidebar-toggle-height: 60px;
$sidebar-milestone-toggle-bottom-margin: 10px;
/* /*
* Buttons * Buttons
......
...@@ -187,7 +187,12 @@ ...@@ -187,7 +187,12 @@
padding-left: 10px; padding-left: 10px;
&:hover { &:hover {
color: $gray-darkest; color: $gl-text-color;
}
&:hover,
&:focus {
text-decoration: none;
} }
} }
...@@ -368,6 +373,14 @@ ...@@ -368,6 +373,14 @@
padding: 15px 0 0; padding: 15px 0 0;
border-bottom: 0; border-bottom: 0;
overflow: hidden; overflow: hidden;
&:hover {
background-color: $sidebar-block-hover-color;
}
&.issuable-sidebar-header {
padding-top: 0;
}
} }
.participants { .participants {
...@@ -380,8 +393,17 @@ ...@@ -380,8 +393,17 @@
.gutter-toggle { .gutter-toggle {
width: 100%; width: 100%;
height: $sidebar-toggle-height;
margin-left: 0; margin-left: 0;
padding-left: 25px; padding-left: 0;
border-bottom: 1px solid $border-gray-dark;
}
a.gutter-toggle {
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
...@@ -428,10 +450,10 @@ ...@@ -428,10 +450,10 @@
.btn-clipboard { .btn-clipboard {
border: 0; border: 0;
background: transparent;
color: $issuable-sidebar-color; color: $issuable-sidebar-color;
&:hover { &:hover {
background: transparent;
color: $gl-text-color; color: $gl-text-color;
} }
} }
......
...@@ -53,10 +53,6 @@ ...@@ -53,10 +53,6 @@
} }
.milestone-sidebar { .milestone-sidebar {
.gutter-toggle {
margin-bottom: 10px;
}
.milestone-progress { .milestone-progress {
.title { .title {
padding-top: 5px; padding-top: 5px;
...@@ -102,7 +98,17 @@ ...@@ -102,7 +98,17 @@
margin-right: 0; margin-right: 0;
} }
.right-sidebar-expanded & {
.gutter-toggle {
margin-bottom: $sidebar-milestone-toggle-bottom-margin;
}
}
.right-sidebar-collapsed & { .right-sidebar-collapsed & {
.milestone-progress {
padding-top: 0;
}
.reference { .reference {
border-top: 1px solid $border-gray-normal; border-top: 1px solid $border-gray-normal;
} }
......
...@@ -9,6 +9,32 @@ module IssuablesHelper ...@@ -9,6 +9,32 @@ module IssuablesHelper
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}" "right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end end
def sidebar_gutter_tooltip_text
sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
end
def sidebar_assignee_tooltip_label(issuable)
if issuable.assignee
issuable.assignee.name
else
issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
end
end
def sidebar_due_date_tooltip_label(issuable)
if issuable.due_date
"#{_('Due date')}<br />#{due_date_remaining_days(issuable)}"
else
_('Due date')
end
end
def due_date_remaining_days(issuable)
remaining_days_in_words = remaining_days_in_words(issuable)
"#{issuable.due_date.to_s(:medium)} (#{remaining_days_in_words})"
end
def multi_label_name(current_labels, default_label) def multi_label_name(current_labels, default_label)
if current_labels && current_labels.any? if current_labels && current_labels.any?
title = current_labels.first.try(:title) title = current_labels.first.try(:title)
...@@ -153,10 +179,14 @@ module IssuablesHelper ...@@ -153,10 +179,14 @@ module IssuablesHelper
def issuable_labels_tooltip(labels, limit: 5) def issuable_labels_tooltip(labels, limit: 5)
first, last = labels.partition.with_index { |_, i| i < limit } first, last = labels.partition.with_index { |_, i| i < limit }
if labels && labels.any?
label_names = first.collect(&:name) label_names = first.collect(&:name)
label_names << "and #{last.size} more" unless last.empty? label_names << "and #{last.size} more" unless last.empty?
label_names.join(', ') label_names.join(', ')
else
_("Labels")
end
end end
def issuables_state_counter_text(issuable_type, state, display_count) def issuables_state_counter_text(issuable_type, state, display_count)
...@@ -321,7 +351,7 @@ module IssuablesHelper ...@@ -321,7 +351,7 @@ module IssuablesHelper
def issuable_todo_button_data(issuable, todo, is_collapsed) def issuable_todo_button_data(issuable, todo, is_collapsed)
{ {
todo_text: "Add todo", todo_text: "Add todo",
mark_text: "Mark done", mark_text: "Mark todo as done",
todo_icon: (is_collapsed ? icon('plus-square') : nil), todo_icon: (is_collapsed ? icon('plus-square') : nil),
mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil), mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
issuable_id: issuable.id, issuable_id: issuable.id,
......
module MilestonesHelper module MilestonesHelper
include EntityDateHelper
def milestones_filter_path(opts = {}) def milestones_filter_path(opts = {})
if @project if @project
project_milestones_path(@project, opts) project_milestones_path(@project, opts)
...@@ -72,6 +74,19 @@ module MilestonesHelper ...@@ -72,6 +74,19 @@ module MilestonesHelper
end end
end end
def milestone_progress_tooltip_text(milestone)
has_issues = milestone.total_issues_count(current_user) > 0
if has_issues
[
_('Progress'),
_("%{percent}%% complete") % { percent: milestone.percent_complete(current_user) }
].join('<br />')
else
_('Progress')
end
end
def milestone_progress_bar(milestone) def milestone_progress_bar(milestone)
options = { options = {
class: 'progress-bar progress-bar-success', class: 'progress-bar progress-bar-success',
...@@ -95,27 +110,69 @@ module MilestonesHelper ...@@ -95,27 +110,69 @@ module MilestonesHelper
end end
def milestone_tooltip_title(milestone) def milestone_tooltip_title(milestone)
if milestone.due_date if milestone
[milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ') "#{milestone.title}<br />#{milestone_tooltip_due_date(milestone)}"
else
_('Milestone')
end end
end end
def milestone_remaining_days(milestone) def milestone_time_for(date, date_type)
if milestone.expired? title = date_type == :start ? "Start date" : "End date"
content_tag(:strong, 'Past due')
elsif milestone.upcoming? if date
content_tag(:strong, 'Upcoming') time_ago = time_ago_in_words(date)
elsif milestone.due_date time_ago.slice!("about ")
time_ago = time_ago_in_words(milestone.due_date)
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" } time_ago << if date.past?
content.slice!("about ") " ago"
content << " remaining" else
content.html_safe " remaining"
elsif milestone.start_date && milestone.start_date.past? end
days = milestone.elapsed_days
content = content_tag(:strong, days) content = [
content << " #{'day'.pluralize(days)} elapsed" title,
"<br />",
date.to_s(:medium),
"(#{time_ago})"
].join(" ")
content.html_safe content.html_safe
else
title
end
end
def milestone_issues_tooltip_text(milestone)
issues = milestone.count_issues_by_state(current_user)
return _("Issues") if issues.empty?
content = []
content << n_("1 open issue", "%d open issues", issues["opened"]) % issues["opened"] if issues["opened"]
content << n_("1 closed issue", "%d closed issues", issues["closed"]) % issues["closed"] if issues["closed"]
content.join('<br />').html_safe
end
def milestone_merge_requests_tooltip_text(milestone)
merge_requests = milestone.merge_requests
return _("Merge requests") if merge_requests.empty?
content = []
content << n_("1 open merge request", "%d open merge requests", merge_requests.opened.count) % merge_requests.opened.count if merge_requests.opened.any?
content << n_("1 closed merge request", "%d closed merge requests", merge_requests.closed.count) % merge_requests.closed.count if merge_requests.closed.any?
content << n_("1 merged merge request", "%d merged merge requests", merge_requests.merged.count) % merge_requests.merged.count if merge_requests.merged.any?
content.join('<br />').html_safe
end
def milestone_tooltip_due_date(milestone)
if milestone.due_date
"#{milestone.due_date.to_s(:medium)} (#{remaining_days_in_words(milestone)})"
end end
end end
......
...@@ -102,14 +102,14 @@ module Milestoneish ...@@ -102,14 +102,14 @@ module Milestoneish
Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate) Gitlab::TimeTrackingFormatter.output(total_issue_time_estimate)
end end
private
def count_issues_by_state(user) def count_issues_by_state(user)
memoize_per_user(user, :count_issues_by_state) do memoize_per_user(user, :count_issues_by_state) do
issues_visible_to_user(user).reorder(nil).group(:state).count issues_visible_to_user(user).reorder(nil).group(:state).count
end end
end end
private
def memoize_per_user(user, method_name) def memoize_per_user(user, method_name)
memoized_users[method_name][user&.id] ||= yield memoized_users[method_name][user&.id] ||= yield
end end
......
module EntityDateHelper module EntityDateHelper
include ActionView::Helpers::DateHelper include ActionView::Helpers::DateHelper
include ActionView::Helpers::TagHelper
def interval_in_words(diff) def interval_in_words(diff)
return 'Not started' unless diff return 'Not started' unless diff
...@@ -34,4 +35,30 @@ module EntityDateHelper ...@@ -34,4 +35,30 @@ module EntityDateHelper
duration_hash duration_hash
end end
# Generates an HTML-formatted string for remaining dates based on start_date and due_date
#
# It returns "Past due" for expired entities
# It returns "Upcoming" for upcoming entities
# If due date is provided, it returns "# days|weeks|months remaining|ago"
# If start date is provided and elapsed, with no due date, it returns "# days elapsed"
def remaining_days_in_words(entity)
if entity.try(:expired?)
content_tag(:strong, 'Past due')
elsif entity.try(:upcoming?)
content_tag(:strong, 'Upcoming')
elsif entity.due_date
is_upcoming = (entity.due_date - Date.today).to_i > 0
time_ago = time_ago_in_words(entity.due_date)
content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
content.slice!("about ")
content << " " + (is_upcoming ? _("remaining") : _("ago"))
content.html_safe
elsif entity.start_date && entity.start_date.past?
days = entity.elapsed_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed"
content.html_safe
end
end
end end
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
- if issue.milestone - if issue.milestone
%span.issuable-milestone.hidden-xs %span.issuable-milestone.hidden-xs
&nbsp; &nbsp;
= link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(issue) } do = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(issue.milestone) } do
= icon('clock-o') = icon('clock-o')
= issue.milestone.title = issue.milestone.title
- if issue.due_date - if issue.due_date
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
- if merge_request.milestone - if merge_request.milestone
%span.issuable-milestone.hidden-xs %span.issuable-milestone.hidden-xs
&nbsp; &nbsp;
= link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: issuable_milestone_tooltip_title(merge_request) } do = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_due_date(merge_request.milestone) } do
= icon('clock-o') = icon('clock-o')
= merge_request.milestone.title = merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch - if merge_request.target_project.default_branch != merge_request.target_branch
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- if current_user - if current_user
%span.issuable-header-text.hide-collapsed.pull-left %span.issuable-header-text.hide-collapsed.pull-left
= _('Todo') = _('Todo')
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
- if current_user - if current_user
= render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
...@@ -19,11 +19,10 @@ ...@@ -19,11 +19,10 @@
.block.assignee .block.assignee
= render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present? = render "shared/issuable/sidebar_assignees", issuable: issuable, can_edit_issuable: can_edit_issuable, signed_in: current_user.present?
.block.milestone .block.milestone
.sidebar-collapsed-icon .sidebar-collapsed-icon.has-tooltip{ title: milestone_tooltip_title(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } }
= icon('clock-o', 'aria-hidden': 'true') = icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title %span.milestone-title
- if issuable.milestone - if issuable.milestone
%span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title = issuable.milestone.title
- else - else
= _('None') = _('None')
...@@ -34,7 +33,7 @@ ...@@ -34,7 +33,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right' = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed .value.hide-collapsed
- if issuable.milestone - if issuable.milestone
= link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 } = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 1 }
- else - else
%span.no-value %span.no-value
= _('None') = _('None')
...@@ -50,7 +49,7 @@ ...@@ -50,7 +49,7 @@
= icon('spinner spin', 'aria-hidden': 'true') = icon('spinner spin', 'aria-hidden': 'true')
- if issuable.has_attribute?(:due_date) - if issuable.has_attribute?(:due_date)
.block.due_date .block.due_date
.sidebar-collapsed-icon .sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 1 }, title: sidebar_due_date_tooltip_label(issuable) }
= icon('calendar', 'aria-hidden': 'true') = icon('calendar', 'aria-hidden': 'true')
%span.js-due-date-sidebar-value %span.js-due-date-sidebar-value
= issuable.due_date.try(:to_s, :medium) || 'None' = issuable.due_date.try(:to_s, :medium) || 'None'
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= _('Assignee') = _('Assignee')
= icon('spinner spin') = icon('spinner spin')
- else - else
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) } .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if issuable.assignee - if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24) = link_to_member(@project, issuable.assignee, size: 24)
- else - else
......
- is_collapsed = local_assigns.fetch(:is_collapsed, false) - is_collapsed = local_assigns.fetch(:is_collapsed, false)
- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark done') - mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark todo as done')
- todo_content = is_collapsed ? icon('plus-square') : _('Add todo') - todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button', %button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'), class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
title: (todo.nil? ? _('Add todo') : _('Mark done')), title: (todo.nil? ? _('Add todo') : _('Mark todo as done')),
'aria-label' => (todo.nil? ? _('Add todo') : _('Mark done')), 'aria-label' => (todo.nil? ? _('Add todo') : _('Mark todo as done')),
data: issuable_todo_button_data(issuable, todo, is_collapsed) } data: issuable_todo_button_data(issuable, todo, is_collapsed) }
%span.issuable-todo-inner.js-issuable-todo-inner< %span.issuable-todo-inner.js-issuable-todo-inner<
- if todo - if todo
......
- merge_request = issuable - merge_request = issuable
.block.assignee .block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (merge_request.assignee.name if merge_request.assignee) } .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- if merge_request.assignee - if merge_request.assignee
= link_to_member(@project, merge_request.assignee, size: 24) = link_to_member(@project, merge_request.assignee, size: 24)
- else - else
......
...@@ -4,12 +4,8 @@ ...@@ -4,12 +4,8 @@
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } %aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar .issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header .block.milestone-progress.issuable-sidebar-header
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } %a.gutter-toggle.pull-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => "Toggle sidebar", title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left' } }
= sidebar_gutter_toggle_icon = sidebar_gutter_toggle_icon
.sidebar-collapsed-icon
%span== #{milestone.percent_complete(current_user)}%
= milestone_progress_bar(milestone)
.title.hide-collapsed .title.hide-collapsed
%strong.bold== #{milestone.percent_complete(current_user)}% %strong.bold== #{milestone.percent_complete(current_user)}%
%span.hide-collapsed %span.hide-collapsed
...@@ -17,6 +13,11 @@ ...@@ -17,6 +13,11 @@
.value.hide-collapsed .value.hide-collapsed
= milestone_progress_bar(milestone) = milestone_progress_bar(milestone)
.block.milestone-progress.hide-expanded
.sidebar-collapsed-icon.has-tooltip{ title: milestone_progress_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%span== #{milestone.percent_complete(current_user)}%
= milestone_progress_bar(milestone)
.block.start_date.hide-collapsed .block.start_date.hide-collapsed
.title .title
Start date Start date
...@@ -35,18 +36,24 @@ ...@@ -35,18 +36,24 @@
%span.collapsed-milestone-date %span.collapsed-milestone-date
- if milestone.start_date && milestone.due_date - if milestone.start_date && milestone.due_date
- if milestone.start_date.year == milestone.due_date.year - if milestone.start_date.year == milestone.due_date.year
.milestone-date= milestone.start_date.strftime('%b %-d') .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d')
- else - else
.milestone-date= milestone.start_date.strftime('%b %-d %Y') .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d %Y')
.date-separator - .date-separator -
.due_date= milestone.due_date.strftime('%b %-d %Y') .due_date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.due_date.strftime('%b %-d %Y')
- elsif milestone.start_date - elsif milestone.start_date
From From
.milestone-date= milestone.start_date.strftime('%b %-d %Y') .milestone-date.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.start_date.strftime('%b %-d %Y')
- elsif milestone.due_date - elsif milestone.due_date
Until Until
.milestone-date= milestone.due_date.strftime('%b %-d %Y') .milestone-date.has-tooltip{ title: milestone_time_for(milestone.due_date, :end), data: { container: 'body', html: 1, placement: 'left' } }
= milestone.due_date.strftime('%b %-d %Y')
- else - else
.has-tooltip{ title: milestone_time_for(milestone.start_date, :start), data: { container: 'body', html: 1, placement: 'left' } }
None None
.title.hide-collapsed .title.hide-collapsed
Due date Due date
...@@ -58,14 +65,14 @@ ...@@ -58,14 +65,14 @@
%span.bold= milestone.due_date.to_s(:medium) %span.bold= milestone.due_date.to_s(:medium)
- else - else
%span.no-value No due date %span.no-value No due date
- remaining_days = milestone_remaining_days(milestone) - remaining_days = remaining_days_in_words(milestone)
- if remaining_days.present? - if remaining_days.present?
= surround '(', ')' do = surround '(', ')' do
%span.remaining-days= remaining_days %span.remaining-days= remaining_days
- if !project || can?(current_user, :read_issue, project) - if !project || can?(current_user, :read_issue, project)
.block.issues .block.issues
.sidebar-collapsed-icon .sidebar-collapsed-icon.has-tooltip{ title: milestone_issues_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong %strong
= custom_icon('issues') = custom_icon('issues')
%span= milestone.issues_visible_to_user(current_user).count %span= milestone.issues_visible_to_user(current_user).count
...@@ -93,7 +100,7 @@ ...@@ -93,7 +100,7 @@
= icon('spinner spin') = icon('spinner spin')
.block.merge-requests .block.merge-requests
.sidebar-collapsed-icon .sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 1, placement: 'left' } }
%strong %strong
= custom_icon('mr_bold') = custom_icon('mr_bold')
%span= milestone.merge_requests.count %span= milestone.merge_requests.count
......
---
title: Improve tooltips in collapsed right sidebar
merge_request: 17714
author:
type: changed
...@@ -28,7 +28,7 @@ Comments and system notes also appear automatically in response to various actio ...@@ -28,7 +28,7 @@ Comments and system notes also appear automatically in response to various actio
#### 2. Todos #### 2. Todos
- Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.html) list - Add todo: add that issue to your [GitLab Todo](../../../workflow/todos.html) list
- Mark done: mark that issue as done (reflects on the Todo list) - Mark todo as done: mark that issue as done (reflects on the Todo list)
#### 3. Assignee #### 3. Assignee
......
...@@ -92,9 +92,9 @@ corresponding **Done** button, and it will disappear from your Todo list. ...@@ -92,9 +92,9 @@ corresponding **Done** button, and it will disappear from your Todo list.
![A Todo in the Todos dashboard](img/todo_list_item.png) ![A Todo in the Todos dashboard](img/todo_list_item.png)
A Todo can also be marked as done from the issue or merge request sidebar using A Todo can also be marked as done from the issue or merge request sidebar using
the "Mark done" button. the "Mark todo as done" button.
![Mark Done from the issuable sidebar](img/todos_mark_done_sidebar.png) ![Mark todo as done from the issuable sidebar](img/todos_mark_done_sidebar.png)
You can mark all your Todos as done at once by clicking on the **Mark all as You can mark all your Todos as done at once by clicking on the **Mark all as
done** button. done** button.
......
...@@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do ...@@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do
it 'creates todo when clicking button' do it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do page.within '.issuable-sidebar' do
click_button 'Add todo' click_button 'Add todo'
expect(page).to have_content 'Mark done' expect(page).to have_content 'Mark todo as done'
end end
page.within '.header-content .todos-count' do page.within '.header-content .todos-count' do
...@@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do ...@@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do
it 'marks a todo as done' do it 'marks a todo as done' do
page.within '.issuable-sidebar' do page.within '.issuable-sidebar' do
click_button 'Add todo' click_button 'Add todo'
click_button 'Mark done' click_button 'Mark todo as done'
end end
expect(page).to have_selector('.todos-count', visible: false) expect(page).to have_selector('.todos-count', visible: false)
......
...@@ -22,11 +22,15 @@ describe IssuablesHelper do ...@@ -22,11 +22,15 @@ describe IssuablesHelper do
end end
describe '#issuable_labels_tooltip' do describe '#issuable_labels_tooltip' do
it 'returns label text' do it 'returns label text with no labels' do
expect(issuable_labels_tooltip([])).to eq("Labels")
end
it 'returns label text with labels within max limit' do
expect(issuable_labels_tooltip([label])).to eq(label.title) expect(issuable_labels_tooltip([label])).to eq(label.title)
end end
it 'returns label text' do it 'returns label text with labels exceeding max limit' do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more") expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end end
end end
......
...@@ -83,58 +83,4 @@ describe MilestonesHelper do ...@@ -83,58 +83,4 @@ describe MilestonesHelper do
end end
end end
end end
describe '#milestone_remaining_days' do
around do |example|
Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
end
context 'when less than 31 days remaining' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
it 'returns days remaining' do
expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
end
end
context 'when less than 1 year and more than 30 days remaining' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
it 'returns months remaining' do
expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
end
end
context 'when more than 1 year remaining' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
it 'returns years remaining' do
expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
end
end
context 'when milestone is expired' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
it 'returns "Past due"' do
expect(milestone_remaining).to eq("<strong>Past due</strong>")
end
end
context 'when milestone has start_date in the future' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
it 'returns "Upcoming"' do
expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
end
end
context 'when milestone has start_date in the past' do
let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
it 'returns days elapsed' do
expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
end
end
end
end end
...@@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { ...@@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => { setTimeout(() => {
expect( expect(
document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
).toBe('Mark done'); ).toBe('Mark todo as done');
done(); done();
}); });
...@@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { ...@@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
setTimeout(() => { setTimeout(() => {
expect( expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'), document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
).toBe('Mark done'); ).toBe('Mark todo as done');
done(); done();
}); });
...@@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => { ...@@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('updates aria-label to mark done', (done) => { it('updates aria-label to mark todo as done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
setTimeout(() => { setTimeout(() => {
expect( expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
).toBe('Mark done'); ).toBe('Mark todo as done');
done(); done();
}); });
...@@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { ...@@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
.then(() => { .then(() => {
expect( expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
).toBe('Mark done'); ).toBe('Mark todo as done');
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
}) })
......
...@@ -96,7 +96,9 @@ describe Milestone do ...@@ -96,7 +96,9 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.prev_year) allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
end end
it { expect(milestone.expired?).to be_truthy } it 'returns true when due_date is in the past' do
expect(milestone.expired?).to be_truthy
end
end end
context "not expired" do context "not expired" do
...@@ -104,17 +106,19 @@ describe Milestone do ...@@ -104,17 +106,19 @@ describe Milestone do
allow(milestone).to receive(:due_date).and_return(Date.today.next_year) allow(milestone).to receive(:due_date).and_return(Date.today.next_year)
end end
it { expect(milestone.expired?).to be_falsey } it 'returns false when due_date is in the future' do
expect(milestone.expired?).to be_falsey
end
end end
end end
describe '#upcoming?' do describe '#upcoming?' do
it 'returns true' do it 'returns true when start_date is in the future' do
milestone = build(:milestone, start_date: Time.now + 1.month) milestone = build(:milestone, start_date: Time.now + 1.month)
expect(milestone.upcoming?).to be_truthy expect(milestone.upcoming?).to be_truthy
end end
it 'returns false' do it 'returns false when start_date is in the past' do
milestone = build(:milestone, start_date: Date.today.prev_year) milestone = build(:milestone, start_date: Date.today.prev_year)
expect(milestone.upcoming?).to be_falsey expect(milestone.upcoming?).to be_falsey
end end
......
...@@ -32,6 +32,7 @@ describe EntityDateHelper do ...@@ -32,6 +32,7 @@ describe EntityDateHelper do
end end
it 'converts 86560 seconds' do it 'converts 86560 seconds' do
Rails.logger.debug date_helper_class.inspect
expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40) expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40)
end end
...@@ -42,4 +43,58 @@ describe EntityDateHelper do ...@@ -42,4 +43,58 @@ describe EntityDateHelper do
it 'converts 986760 seconds' do it 'converts 986760 seconds' do
expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6) expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6)
end end
describe '#remaining_days_in_words' do
around do |example|
Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
end
context 'when less than 31 days remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
it 'returns days remaining' do
expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
end
end
context 'when less than 1 year and more than 30 days remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
it 'returns months remaining' do
expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
end
end
context 'when more than 1 year remaining' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
it 'returns years remaining' do
expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
end
end
context 'when milestone is expired' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
it 'returns "Past due"' do
expect(milestone_remaining).to eq("<strong>Past due</strong>")
end
end
context 'when milestone has start_date in the future' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
it 'returns "Upcoming"' do
expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
end
end
context 'when milestone has start_date in the past' do
let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
it 'returns days elapsed' do
expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
end
end
end
end end
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