Skip to content
Snippets Groups Projects
Commit baa37edd authored by Constance Okoghenun's avatar Constance Okoghenun Committed by Phil Hughes
Browse files

Resolve "Issue board card design"

parent 06e8cf58
No related branches found
No related tags found
No related merge requests found
Showing
with 541 additions and 170 deletions
<script>
import $ from 'jquery';
import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
import tooltip from '../../vue_shared/directives/tooltip';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
 
export default {
components: {
UserAvatarLink,
Icon,
UserAvatarLink,
TooltipOnTruncate,
IssueDueDate,
IssueTimeEstimate,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
issue: {
Loading
Loading
@@ -45,8 +51,8 @@ export default {
},
data() {
return {
limitBeforeCounter: 3,
maxRender: 4,
limitBeforeCounter: 2,
maxRender: 3,
maxCounter: 99,
};
},
Loading
Loading
@@ -55,7 +61,9 @@ export default {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
return `${this.assigneeCounterLabel} more`;
const { numberOverLimit, maxCounter } = this;
const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
return sprintf(__('%{count} more assignees'), { count });
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
Loading
Loading
@@ -80,6 +88,10 @@ export default {
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
issueReferencePath() {
const { referencePath, groupId } = this.issue;
return !groupId ? referencePath.split('#')[0] : null;
},
},
methods: {
isIndexLessThanlimit(index) {
Loading
Loading
@@ -96,11 +108,9 @@ export default {
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
if (!assignee) return '';
return `${this.rootPath}${assignee.username}`;
},
assigneeUrlTitle(assignee) {
return `Assigned to ${assignee.name}`;
},
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
Loading
Loading
@@ -108,19 +118,29 @@ export default {
if (!label.id) return false;
return true;
},
filterByLabel(label, e) {
filterByLabel(label) {
if (!this.updateFilters) return;
const labelTitle = encodeURIComponent(label.title);
const filter = `label_name[]=${labelTitle}`;
this.applyFilter(filter);
},
filterByWeight(weight) {
if (!this.updateFilters) return;
 
const issueWeight = encodeURIComponent(weight);
const filter = `weight=${issueWeight}`;
this.applyFilter(filter);
},
applyFilter(filter) {
const filterPath = boardsStore.filter.path.split('&');
const labelTitle = encodeURIComponent(label.title);
const param = `label_name[]=${labelTitle}`;
const labelIndex = filterPath.indexOf(param);
$(e.currentTarget).tooltip('hide');
const filterIndex = filterPath.indexOf(filter);
 
if (labelIndex === -1) {
filterPath.push(param);
if (filterIndex === -1) {
filterPath.push(filter);
} else {
filterPath.splice(labelIndex, 1);
filterPath.splice(filterIndex, 1);
}
 
boardsStore.filter.path = filterPath.join('&');
Loading
Loading
@@ -141,24 +161,62 @@ export default {
<template>
<div>
<div class="board-card-header">
<h4 class="board-card-title">
<h4 class="board-card-title append-bottom-0 prepend-top-0">
<icon
v-if="issue.confidential"
v-gl-tooltip
name="eye-slash"
class="confidential-icon"
/>
<a
:title="__('Confidential')"
class="confidential-icon append-right-4"
:aria-label="__('Confidential')"
/><a
:href="issue.path"
:title="issue.title"
class="js-no-trigger"
@mousemove.stop>{{ issue.title }}</a>
</h4>
</div>
<div
v-if="showLabelFooter"
class="board-card-labels prepend-top-4 d-flex flex-wrap"
>
<button
v-for="label in issue.labels"
v-if="showLabel(label)"
:key="label.id"
v-gl-tooltip
:style="labelStyle(label)"
:title="label.description"
class="badge color-label append-right-4 prepend-top-4"
type="button"
@click="filterByLabel(label)"
>
{{ label.title }}
</button>
</div>
<div class="board-card-footer d-flex justify-content-between align-items-end">
<div class="d-flex align-items-start flex-wrap-reverse board-card-number-container js-board-card-number-container">
<span
v-if="issueId"
class="board-card-number append-right-5"
v-if="issue.referencePath"
class="board-card-number d-flex append-right-8 prepend-top-8"
>
{{ issue.referencePath }}
<tooltip-on-truncate
v-if="issueReferencePath"
:title="issueReferencePath"
placement="bottom"
class="board-issue-path block-truncated bold"
>{{ issueReferencePath }}</tooltip-on-truncate>#{{ issue.iid }}
</span>
</h4>
<span class="board-info-items prepend-top-8 d-inline-block">
<issue-due-date
v-if="issue.dueDate"
:date="issue.dueDate"
/><issue-time-estimate
v-if="issue.timeEstimate"
:estimate="issue.timeEstimate"
/>
</span>
</div>
<div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
Loading
Loading
@@ -167,38 +225,26 @@ export default {
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar"
:tooltip-text="assigneeUrlTitle(assignee)"
:img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
/>
>
<span class="js-assignee-tooltip">
<span class="bold d-block">Assignee</span>
{{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span>
</span>
</user-avatar-link>
<span
v-if="shouldRenderCounter"
v-tooltip
v-gl-tooltip
:title="assigneeCounterTooltip"
class="avatar-counter"
data-placement="bottom"
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
<div
v-if="showLabelFooter"
class="board-card-footer"
>
<button
v-for="label in issue.labels"
v-if="showLabel(label)"
:key="label.id"
v-tooltip
:style="labelStyle(label)"
:title="label.description"
class="badge color-label"
type="button"
data-container="body"
@click="filterByLabel(label, $event)"
>
{{ label.title }}
</button>
</div>
</div>
</template>
<script>
import dateFormat from 'dateformat';
import { GlTooltip } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
export default {
components: {
Icon,
GlTooltip,
},
props: {
date: {
type: String,
required: true,
},
},
computed: {
title() {
const timeago = getTimeago();
const { timeDifference, standardDateFormat } = this;
const formatedDate = standardDateFormat;
if (timeDifference >= -1 && timeDifference < 7) {
return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
}
return timeago.format(this.issueDueDate);
},
body() {
const { timeDifference, issueDueDate, standardDateFormat } = this;
if (timeDifference === 0) {
return __('Today');
} else if (timeDifference === 1) {
return __('Tomorrow');
} else if (timeDifference === -1) {
return __('Yesterday');
} else if (timeDifference > 0 && timeDifference < 7) {
return dateFormat(issueDueDate, 'dddd', true);
}
return standardDateFormat;
},
issueDueDate() {
return new Date(this.date);
},
timeDifference() {
const today = new Date();
return getDayDifference(today, this.issueDueDate);
},
isPastDue() {
if (this.timeDifference >= 0) return false;
return true;
},
standardDateFormat() {
const today = new Date();
const isDueInCurrentYear = today.getFullYear() === this.issueDueDate.getFullYear();
return dateInWords(this.issueDueDate, true, isDueInCurrentYear);
},
},
};
</script>
<template>
<span>
<span
ref="issueDueDate"
class="board-card-info card-number"
>
<icon
:class="{'text-danger': isPastDue, 'board-card-info-icon': true}"
name="calendar"
/><time
:class="{'text-danger': isPastDue}"
datetime="date"
class="board-card-info-text">{{ body }}</time>
</span>
<gl-tooltip
:target="() => $refs.issueDueDate"
placement="bottom"
>
<span class="bold">{{ __('Due date') }}</span>
<br />
<span :class="{'text-danger-muted': isPastDue}">{{ title }}</span>
</gl-tooltip>
</span>
</template>
<script>
import { GlTooltip } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
export default {
components: {
Icon,
GlTooltip,
},
props: {
estimate: {
type: Number,
required: true,
},
},
computed: {
title() {
return stringifyTime(parseSeconds(this.estimate), true);
},
timeEstimate() {
return stringifyTime(parseSeconds(this.estimate));
},
},
};
</script>
<template>
<span>
<span
ref="issueTimeEstimate"
class="board-card-info card-number"
>
<icon
name="hourglass"
css-classes="board-card-info-icon"
/><time class="board-card-info-text">{{ timeEstimate }}</time>
</span>
<gl-tooltip
:target="() => $refs.issueTimeEstimate"
placement="bottom"
class="js-issue-time-estimate"
>
<span class="bold d-block">{{ __('Time estimate') }}</span>
{{ title }}
</gl-tooltip>
</span>
</template>
Loading
Loading
@@ -30,6 +30,7 @@ class ListIssue {
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
this.timeEstimate = obj.time_estimate;
this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
 
if (obj.project) {
Loading
Loading
Loading
Loading
@@ -454,12 +454,20 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
/**
* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
* If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
*/
export const stringifyTime = timeObject => {
export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
if (fullNameFormat && isNonZero) {
// Remove traling 's' if unit value is singular
const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
return `${memo} ${unitValue} ${formatedUnitName}`;
}
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
},
'',
Loading
Loading
Loading
Loading
@@ -15,14 +15,14 @@
 
*/
 
import { GlTooltip } from '@gitlab-org/gitlab-ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
import tooltip from '../../directives/tooltip';
 
export default {
name: 'UserAvatarImage',
directives: {
tooltip,
components: {
GlTooltip,
},
props: {
lazy: {
Loading
Loading
@@ -73,9 +73,6 @@ export default {
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
tooltipContainer() {
return this.tooltipText ? 'body' : null;
},
avatarSizeClass() {
return `s${this.size}`;
},
Loading
Loading
@@ -84,22 +81,30 @@ export default {
</script>
 
<template>
<img
v-tooltip
:class="{
lazy: lazy,
[avatarSizeClass]: true,
[cssClasses]: true
}"
:src="resultantSrcAttribute"
:width="size"
:height="size"
:alt="imgAlt"
:data-src="sanitizedSource"
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
class="avatar"
data-boundary="window"
/>
<span>
<img
ref="userAvatarImage"
:class="{
lazy: lazy,
[avatarSizeClass]: true,
[cssClasses]: true
}"
:src="resultantSrcAttribute"
:width="size"
:height="size"
:alt="imgAlt"
:data-src="sanitizedSource"
class="avatar"
/>
<gl-tooltip
:target="() => $refs.userAvatarImage"
:placement="tooltipPlacement"
boundary="window"
class="js-user-avatar-image-toolip"
>
<slot>
{{ tooltipText }}
</slot>
</gl-tooltip>
</span>
</template>
Loading
Loading
@@ -17,9 +17,8 @@
 
*/
 
import { GlLink } from '@gitlab-org/gitlab-ui';
import { GlLink, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import userAvatarImage from './user_avatar_image.vue';
import tooltip from '../../directives/tooltip';
 
export default {
name: 'UserAvatarLink',
Loading
Loading
@@ -28,7 +27,7 @@ export default {
userAvatarImage,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
linkHref: {
Loading
Loading
@@ -94,11 +93,14 @@ export default {
:size="imgSize"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
/><span
>
<slot></slot>
</user-avatar-image><span
v-if="shouldShowUsername"
v-tooltip
v-gl-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
class="js-user-avatar-link-username"
>{{ username }}</span><slot name="avatar-badge"></slot>
</gl-link>
</template>
Loading
Loading
@@ -33,6 +33,11 @@
color: $brand-danger;
}
 
.text-danger-muted,
.text-danger-muted:hover {
color: $red-300;
}
.text-warning,
.text-warning:hover {
color: $brand-warning;
Loading
Loading
@@ -345,6 +350,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
.prepend-top-4 { margin-top: $gl-padding-4; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
Loading
Loading
@@ -365,6 +371,7 @@ img.emoji {
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
.append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
Loading
Loading
Loading
Loading
@@ -195,6 +195,7 @@ $well-light-text-color: #5b6169;
* Text
*/
$gl-font-size: 14px;
$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
Loading
Loading
@@ -440,7 +441,7 @@ $ci-skipped-color: #888;
* Boards
*/
$issue-boards-font-size: 14px;
$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
/*
The following heights are used in boards.scss and are used for calculation of the board height.
They probably should be derived in a smarter way.
Loading
Loading
Loading
Loading
@@ -90,20 +90,14 @@
}
 
.with-performance-bar & {
height: calc(
100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}
);
height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
 
@include media-breakpoint-only(sm) {
height: calc(
100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}
);
height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
}
 
@include media-breakpoint-up(md) {
height: calc(
100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}
);
height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
}
}
}
Loading
Loading
@@ -271,7 +265,7 @@
height: 100%;
width: 100%;
margin-bottom: 0;
padding: 5px;
padding: $gl-padding-4;
list-style: none;
overflow-y: auto;
overflow-x: hidden;
Loading
Loading
@@ -284,14 +278,16 @@
 
.board-card {
position: relative;
padding: 11px 10px 11px $gl-padding;
padding: $gl-padding;
background: $white-light;
border-radius: $border-radius-default;
border: 1px solid $theme-gray-200;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
line-height: $gl-padding;
 
&:not(:last-child) {
margin-bottom: 5px;
margin-bottom: $gl-padding-8;
}
 
&.is-active,
Loading
Loading
@@ -302,113 +298,120 @@
.badge {
border: 0;
outline: 0;
&:hover {
text-decoration: underline;
}
@include media-breakpoint-down(lg) {
font-size: $gl-font-size-xs;
padding-left: $gl-padding-4;
padding-right: $gl-padding-4;
font-weight: $gl-font-weight-bold;
}
}
svg {
vertical-align: top;
}
 
.confidential-icon {
vertical-align: text-top;
margin-right: 5px;
color: $orange-600;
cursor: help;
}
@include media-breakpoint-down(md) {
padding: $gl-padding-8;
}
}
 
.board-card-title {
@include overflow-break-word();
margin: 0 30px 0 0;
font-size: 1em;
line-height: inherit;
 
a {
color: $gl-text-color;
margin-right: 2px;
}
@include media-breakpoint-down(md) {
font-size: $label-font-size;
}
}
 
.board-card-header {
display: flex;
min-height: 20px;
.board-card-assignee {
display: flex;
justify-content: flex-end;
position: absolute;
right: 15px;
height: 20px;
width: 20px;
}
 
.avatar-counter {
display: none;
vertical-align: middle;
min-width: 20px;
line-height: 19px;
height: 20px;
padding-left: 2px;
padding-right: 2px;
border-radius: 2em;
}
.board-card-assignee {
display: flex;
margin-top: -$gl-padding-4;
margin-bottom: -$gl-padding-4;
.avatar-counter {
vertical-align: middle;
line-height: $gl-padding-24;
min-width: $gl-padding-24;
height: $gl-padding-24;
border-radius: $gl-padding-24;
background-color: $gl-text-color-tertiary;
font-size: $gl-font-size-xs;
cursor: help;
font-weight: $gl-font-weight-bold;
margin-left: -$gl-padding-4;
border: 0;
padding: 0 $gl-padding-4;
 
img {
vertical-align: top;
@include media-breakpoint-down(md) {
min-width: auto;
height: $gl-padding;
border-radius: $gl-padding;
line-height: $gl-padding;
}
}
 
a {
position: relative;
margin-left: -15px;
}
img {
vertical-align: top;
}
 
a:nth-child(1) {
z-index: 3;
}
.user-avatar-link:not(:only-child) {
margin-left: -$gl-padding-4;
 
a:nth-child(2) {
&:nth-of-type(1) {
z-index: 2;
}
 
a:nth-child(3) {
&:nth-of-type(2) {
z-index: 1;
}
}
 
a:nth-child(4) {
display: none;
}
&:hover {
.avatar-counter {
display: inline-block;
}
a {
position: static;
background-color: $white-light;
transition: background-color 0s;
margin-left: auto;
&:nth-child(4) {
display: block;
}
.avatar {
margin: 0;
 
&:first-child:not(:only-child) {
box-shadow: -10px 0 10px 1px $white-light;
}
}
@include media-breakpoint-down(md) {
width: $gl-padding;
height: $gl-padding;
}
}
 
.avatar {
margin: 0;
@include media-breakpoint-down(md) {
margin-top: 0;
margin-bottom: 0;
}
}
 
.board-card-footer {
margin: 0 0 5px;
.board-card-number {
font-size: $gl-font-size-xs;
color: $gl-text-color-secondary;
overflow: hidden;
 
.badge {
margin-top: 5px;
margin-right: 6px;
@include media-breakpoint-up(md) {
font-size: $label-font-size;
}
}
 
.board-card-number {
font-size: 12px;
color: $gl-text-color-secondary;
.board-card-number-container {
overflow: hidden;
}
 
.issue-boards-search {
Loading
Loading
@@ -474,8 +477,7 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
transition: width $sidebar-transition-duration,
padding $sidebar-transition-duration;
transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
}
 
&.boards-sidebar-slide-enter,
Loading
Loading
@@ -650,3 +652,36 @@
}
}
}
.board-card-info {
color: $gl-text-color-secondary;
white-space: nowrap;
margin-right: $gl-padding-8;
&:not(.board-card-weight) {
cursor: help;
}
&.board-card-weight {
color: $gl-text-color;
cursor: pointer;
&:hover {
color: initial;
text-decoration: underline;
}
}
.board-card-info-icon {
color: $theme-gray-600;
margin-right: $gl-padding-4;
}
@include media-breakpoint-down(md) {
font-size: $label-font-size;
}
}
.board-issue-path.js-show-tooltip {
cursor: help;
}
Loading
Loading
@@ -12,6 +12,7 @@ class IssueBoardEntity < Grape::Entity
expose :project_id
expose :relative_position
expose :weight, if: -> (*) { respond_to?(:weight) }
expose :time_estimate
 
expose :project do |issue|
API::Entities::Project.represent issue.project, only: [:id, :path]
Loading
Loading
---
title: Issue board card design
merge_request: 21229
author:
type: changed
Loading
Loading
@@ -103,6 +103,9 @@ msgstr ""
msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
msgstr ""
 
msgid "%{count} more assignees"
msgstr ""
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
Loading
Loading
@@ -6371,6 +6374,9 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
 
msgid "Time estimate"
msgstr ""
msgid "Time remaining"
msgstr ""
 
Loading
Loading
@@ -6585,6 +6591,9 @@ msgstr ""
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
 
msgid "Today"
msgstr ""
msgid "Todo"
msgstr ""
 
Loading
Loading
@@ -6618,6 +6627,9 @@ msgstr ""
msgid "Token"
msgstr ""
 
msgid "Tomorrow"
msgstr ""
msgid "Too many changes to show."
msgstr ""
 
Loading
Loading
@@ -7086,6 +7098,9 @@ msgstr ""
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
 
msgid "Yesterday"
msgstr ""
msgid "You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -160,7 +160,7 @@ describe 'Issue Boards add issue modal', :js do
 
it 'changes button text with plural' do
page.within('.add-issues-modal') do
all('.board-card .board-card-number').each do |el|
all('.board-card .js-board-card-number-container').each do |el|
el.click
end
 
Loading
Loading
Loading
Loading
@@ -78,7 +78,7 @@ describe 'Issue Boards', :js do
end
 
it 'moves from bottom to top' do
drag(from_index: 2, to_index: 0)
drag(from_index: 2, to_index: 0, duration: 1020)
 
wait_for_requests
 
Loading
Loading
@@ -130,7 +130,7 @@ describe 'Issue Boards', :js do
end
 
it 'moves to bottom of another list' do
drag(list_from_index: 1, list_to_index: 2, to_index: 2)
drag(list_from_index: 1, list_to_index: 2, to_index: 2, duration: 1020)
 
wait_for_requests
 
Loading
Loading
Loading
Loading
@@ -89,16 +89,17 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
 
expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
expect(page).to have_selector('.js-diff-comment-avatar img', count: 1)
end
end
 
it 'shows comment on note avatar' do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
first('.js-diff-comment-avatar img').hover
end
expect(page).to have_content "#{note.author.name}: #{note.note.truncate(17)}"
end
 
it 'toggles comments when clicking avatar' do
Loading
Loading
@@ -109,7 +110,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
expect(page).not_to have_selector('.notes_holder')
 
page.within find_line(position.line_code(project.repository)) do
first('img.js-diff-comment-avatar').click
first('.js-diff-comment-avatar img').click
end
 
expect(page).to have_selector('.notes_holder')
Loading
Loading
@@ -125,7 +126,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
wait_for_requests
 
page.within find_line(position.line_code(project.repository)) do
expect(page).not_to have_selector('img.js-diff-comment-avatar')
expect(page).not_to have_selector('.js-diff-comment-avatar img')
end
end
 
Loading
Loading
@@ -143,7 +144,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
 
expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
expect(page).to have_selector('.js-diff-comment-avatar img', count: 2)
end
end
 
Loading
Loading
@@ -162,7 +163,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
 
expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
expect(page).to have_selector('.js-diff-comment-avatar img', count: 3)
expect(find('.diff-comments-more-count')).to have_content '+1'
end
end
Loading
Loading
Loading
Loading
@@ -8,6 +8,7 @@
"due_date": { "type": "date" },
"project_id": { "type": "integer" },
"relative_position": { "type": ["integer", "null"] },
"time_estimate": { "type": "integer" },
"weight": { "type": "integer" },
"project": {
"type": "object",
Loading
Loading
Loading
Loading
@@ -13,6 +13,7 @@
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
"time_estimate": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" },
"assignable_labels_endpoint": { "type": "string" },
Loading
Loading
import Vue from 'vue';
import dateFormat from 'dateformat';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Issue Due Date component', () => {
let vm;
let date;
const Component = Vue.extend(IssueDueDate);
const createComponent = (dueDate = new Date()) =>
mountComponent(Component, { date: dateFormat(dueDate, 'yyyy-mm-dd', true) });
beforeEach(() => {
date = new Date();
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
it('should render "Today" if the due date is today', () => {
const timeContainer = vm.$el.querySelector('time');
expect(timeContainer.textContent.trim()).toEqual('Today');
});
it('should render "Yesterday" if the due date is yesterday', () => {
date.setDate(date.getDate() - 1);
vm = createComponent(date);
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Yesterday');
});
it('should render "Tomorrow" if the due date is one day from now', () => {
date.setDate(date.getDate() + 1);
vm = createComponent(date);
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Tomorrow');
});
it('should render day of the week if due date is one week away', () => {
date.setDate(date.getDate() + 5);
vm = createComponent(date);
expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true));
});
it('should render month and day for other dates', () => {
date.setDate(date.getDate() + 17);
vm = createComponent(date);
expect(vm.$el.querySelector('time').textContent.trim()).toEqual(
dateFormat(date, 'mmm d', true),
);
});
it('should contain the correct `.text-danger` css class for overdue issue', () => {
date.setDate(date.getDate() - 17);
vm = createComponent(date);
expect(vm.$el.querySelector('time').classList.contains('text-danger')).toEqual(true);
});
});
import Vue from 'vue';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Issue Tine Estimate component', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(IssueTimeEstimate);
vm = mountComponent(Component, {
estimate: 374460,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders the correct time estimate', () => {
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
});
it('renders expanded time estimate in tooltip', () => {
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
'2 weeks 3 days 1 minute',
);
});
it('prevents tooltip xss', done => {
const alertSpy = spyOn(window, 'alert');
vm.estimate = 'Foo <script>alert("XSS")</script>';
vm.$nextTick(() => {
expect(alertSpy).not.toHaveBeenCalled();
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
done();
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment