Commit 80a27ad9 authored by Florie Guibert's avatar Florie Guibert Committed by Kushal Pandya

Swimlanes - Detailed popover for epics

Add date and link to epic in popover hover epic title
parent e0e69938
......@@ -5,10 +5,11 @@ import {
GlLabel,
GlTooltip,
GlIcon,
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
import isWipLimitsOn from 'ee_else_ce/boards/mixins/is_wip_limits';
import { s__, __, sprintf } from '~/locale';
import { n__, s__ } from '~/locale';
import AccessorUtilities from '../../lib/utils/accessor';
import BoardDelete from './board_delete';
import IssueCount from './issue_count.vue';
......@@ -25,6 +26,7 @@ export default {
GlLabel,
GlTooltip,
GlIcon,
GlSprintf,
IssueCount,
},
directives: {
......@@ -82,10 +84,20 @@ export default {
this.listType !== ListType.promotion
);
},
issuesTooltip() {
showMilestoneListDetails() {
return (
this.list.type === 'milestone' &&
this.list.milestone &&
(this.list.isExpanded || !this.isSwimlanesHeader)
);
},
showAssigneeListDetails() {
return this.list.type === 'assignee' && (this.list.isExpanded || !this.isSwimlanesHeader);
},
issuesTooltipLabel() {
const { issuesSize } = this.list;
return sprintf(__('%{issuesSize} issues'), { issuesSize });
return n__(`%d issue`, `%d issues`, issuesSize);
},
chevronTooltip() {
return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
......@@ -111,6 +123,9 @@ export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `boards.${this.boardId}.${this.listType}.${this.list.id}`;
},
collapsedTooltipTitle() {
return this.listTitle || this.listAssignee;
},
},
methods: {
showScopedLabels(label) {
......@@ -147,7 +162,7 @@ export default {
'has-border': list.label && list.label.color,
'gl-relative': list.isExpanded,
'gl-h-full': !list.isExpanded,
'board-inner gl-rounded-base gl-border-b-0': isSwimlanesHeader,
'board-inner gl-rounded-base': isSwimlanesHeader,
}"
:style="{ borderTopColor: list.label && list.label.color ? list.label.color : null }"
class="board-header gl-relative"
......@@ -157,7 +172,9 @@ export default {
<h3
:class="{
'user-can-drag': !disabled && !list.preset,
'gl-border-b-0': !list.isExpanded,
'gl-py-3': !list.isExpanded && !isSwimlanesHeader,
'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
'gl-py-2': !list.isExpanded && isSwimlanesHeader,
}"
class="board-title gl-m-0 gl-display-flex js-board-handle"
>
......@@ -167,21 +184,17 @@ export default {
:aria-label="chevronTooltip"
:title="chevronTooltip"
:icon="chevronIcon"
class="board-title-caret no-drag"
class="board-title-caret no-drag gl-cursor-pointer "
variant="link"
@click="toggleExpanded"
/>
<!-- The following is only true in EE and if it is a milestone -->
<span
v-if="list.type === 'milestone' && list.milestone"
aria-hidden="true"
class="gl-mr-2 milestone-icon"
>
<span v-if="showMilestoneListDetails" aria-hidden="true" class="gl-mr-2 milestone-icon">
<gl-icon name="timer" />
</span>
<a
v-if="list.type === 'assignee'"
v-if="showAssigneeListDetails"
:href="list.assignee.path"
class="user-avatar-link js-no-trigger"
>
......@@ -195,7 +208,10 @@ export default {
width="20"
/>
</a>
<div class="board-title-text">
<div
class="board-title-text"
:class="{ 'gl-display-none': !list.isExpanded && isSwimlanesHeader }"
>
<span
v-if="list.type !== 'label'"
v-gl-tooltip.hover
......@@ -208,7 +224,7 @@ export default {
{{ list.title }}
</span>
<span v-if="list.type === 'assignee'" class="board-title-sub-text gl-ml-2">
@{{ list.assignee.username }}
@{{ listAssignee }}
</span>
<gl-label
v-if="list.type === 'label'"
......@@ -220,6 +236,33 @@ export default {
:title="list.label.title"
/>
</div>
<span
v-if="isSwimlanesHeader && !list.isExpanded"
ref="collapsedInfo"
aria-hidden="true"
class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-700"
>
<gl-icon name="information" />
</span>
<gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo">
<div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div>
<div v-if="list.maxIssueCount !== 0">
&#8226;
<gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')">
<template #issuesSize>{{ issuesTooltipLabel }}</template>
<template #maxIssueCount>{{ list.maxIssueCount }}</template>
</gl-sprintf>
</div>
<div v-else>&#8226; {{ issuesTooltipLabel }}</div>
<div v-if="weightFeatureAvailable">
&#8226;
<gl-sprintf :message="__('%{totalWeight} total weight')">
<template #totalWeight>{{ list.totalWeight }}</template>
</gl-sprintf>
</div>
</gl-tooltip>
<board-delete
v-if="canAdminList && !list.preset && list.id"
:list="list"
......@@ -229,7 +272,7 @@ export default {
v-gl-tooltip.hover.bottom
:class="{ 'gl-display-none': !list.isExpanded }"
:aria-label="__('Delete list')"
class="board-delete no-drag gl-pr-0 gl-shadow-none gl-mr-3"
class="board-delete no-drag gl-pr-0 gl-shadow-none! gl-mr-3"
:title="__('Delete list')"
icon="remove"
size="small"
......@@ -239,9 +282,10 @@ export default {
<div
v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-pr-0 no-drag text-secondary"
:class="{ 'gl-display-none': !list.isExpanded && isSwimlanesHeader }"
>
<span class="gl-display-inline-flex">
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltip" />
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
<span ref="issueCount" class="issue-count-badge-count">
<gl-icon class="gl-mr-2" name="issues" />
<issue-count :issues-size="list.issuesSize" :max-issue-count="list.maxIssueCount" />
......
......@@ -82,7 +82,6 @@
}
.board-title-caret {
cursor: pointer;
border-radius: $border-radius-default;
line-height: $gl-spacing-scale-5;
height: $gl-spacing-scale-5;
......@@ -109,7 +108,6 @@
.board-title {
flex-direction: column;
height: 100%;
padding: $gl-padding-8 0;
}
.board-title-caret {
......@@ -203,8 +201,7 @@
flex-grow: 1;
}
.board-delete {
color: $gray-darkest;
.board-delete.gl-button {
background-color: transparent;
outline: 0;
......@@ -581,5 +578,29 @@
.board-epics-swimlanes {
overflow-x: auto;
min-height: 600px;
min-height: calc(100vh - #{$issue-board-list-difference-xs});
@include media-breakpoint-only(sm) {
min-height: calc(100vh - #{$issue-board-list-difference-sm});
}
@include media-breakpoint-up(md) {
min-height: calc(100vh - #{$issue-board-list-difference-md});
}
.with-performance-bar & {
min-height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
@include media-breakpoint-only(sm) {
min-height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
}
@include media-breakpoint-up(md) {
min-height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
}
}
}
.board-header-collapsed-info-icon:hover {
color: $gray-900;
}
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { formatDate } from '~/lib/utils/datetime_utility';
import { statusType } from '../../epic/constants';
export default {
components: {
GlIcon,
GlLink,
GlPopover,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
epic: {
type: Object,
......@@ -16,18 +22,36 @@ export default {
},
},
computed: {
isOpen() {
return this.epic.state === statusType.open;
},
stateText() {
return this.epic.state === 'opened' ? __('Opened') : __('Closed');
return this.isOpen ? __('Opened') : __('Closed');
},
epicIcon() {
return this.isOpen ? 'epic' : 'epic-closed';
},
stateIconClass() {
return this.epic.state === 'opened' ? 'gl-text-green-500' : 'gl-text-blue-500';
return this.isOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
},
issuesCount() {
const { openedIssues, closedIssues } = this.epic.descendantCounts;
return openedIssues + closedIssues;
},
issuesCountTooltipText() {
return sprintf(__(`%{issuesCount} issues in this group`), { issuesCount: this.issuesCount });
return n__(`%d issue in this group`, `%d issues in this group`, this.issuesCount);
},
epicTimeAgoString() {
return this.isOpen
? sprintf(__(`Opened %{epicTimeagoDate}`), {
epicTimeagoDate: this.timeFormatted(this.epic.createdAt),
})
: sprintf(__(`Closed %{epicTimeagoDate}`), {
epicTimeagoDate: this.timeFormatted(this.epic.closedAt),
});
},
epicDateString() {
return formatDate(this.epic.createdAt);
},
},
};
......@@ -38,16 +62,23 @@ export default {
<gl-icon
class="gl-mr-2 gl-flex-shrink-0"
:class="stateIconClass"
name="epic"
:name="epicIcon"
:aria-label="stateText"
/>
<span
v-gl-tooltip.hover
:title="epic.title"
ref="epicTitle"
class="gl-mr-3 gl-font-weight-bold gl-white-space-nowrap gl-text-overflow-ellipsis gl-overflow-hidden"
>
{{ epic.title }}
</span>
<gl-popover :target="() => $refs.epicTitle" triggers="hover" placement="top">
<template #title
>{{ epic.title }} &middot; {{ epic.reference }}</template
>
<p class="gl-m-0">{{ epicTimeAgoString }}</p>
<p class="gl-mb-2">{{ epicDateString }}</p>
<gl-link :href="epic.webUrl" class="gl-font-sm">{{ __('Go to epic') }}</gl-link>
</gl-popover>
<span
v-gl-tooltip.hover
:title="issuesCountTooltipText"
......
......@@ -45,7 +45,7 @@ export default {
'is-expandable': list.isExpandable,
'is-collapsed': !list.isExpanded,
}"
class="board gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
class="board gl-px-3 gl-vertical-align-top gl-white-space-normal"
>
<board-list-header
:can-admin-list="canAdminList"
......@@ -56,5 +56,12 @@ export default {
/>
</div>
<epic-lane v-for="epic in epics" :key="epic.id" :epic="epic" />
<div class="board-lane-unassigned-issue gl-py-5 gl-px-3 gl-display-flex gl-align-items-center">
<span
class="gl-mr-3 gl-font-weight-bold gl-white-space-nowrap gl-text-overflow-ellipsis gl-overflow-hidden"
>
{{ __('Issues with no epics assigned') }}
</span>
</div>
</div>
</template>
......@@ -6,7 +6,10 @@ query groupEpicsEE($fullPath: ID!) {
iid
title
state
reference
webUrl
createdAt
closedAt
descendantCounts {
openedIssues
closedIssues
......
......@@ -45,6 +45,7 @@ describe('Board List Header Component', () => {
listType = ListType.backlog,
collapsed = false,
withLocalStorage = true,
isSwimlanesHeader = false,
} = {}) => {
const boardId = '1';
......@@ -78,6 +79,7 @@ describe('Board List Header Component', () => {
issueLinkBase: '/',
rootPath: '/',
list,
isSwimlanesHeader,
},
});
};
......@@ -151,5 +153,13 @@ describe('Board List Header Component', () => {
});
});
});
describe('Swimlanes header', () => {
it('when collapsed, it displays info icon', () => {
createComponent({ isSwimlanesHeader: true, collapsed: true });
expect(wrapper.contains('.board-header-collapsed-info-icon')).toBe(true);
});
});
});
});
......@@ -174,6 +174,11 @@ msgid_plural "%d issues"
msgstr[0] ""
msgstr[1] ""
msgid "%d issue in this group"
msgid_plural "%d issues in this group"
msgstr[0] ""
msgstr[1] ""
msgid "%d issue selected"
msgid_plural "%d issues selected"
msgstr[0] ""
......@@ -386,13 +391,10 @@ msgstr ""
msgid "%{issuableType} will be removed! Are you sure?"
msgstr ""
msgid "%{issuesCount} issues in this group"
msgstr ""
msgid "%{issuesSize} issues"
msgid "%{issuesSize} issues with a limit of %{maxIssueCount}"
msgstr ""
msgid "%{issuesSize} issues with a limit of %{maxIssueCount}"
msgid "%{issuesSize} with a limit of %{maxIssueCount}"
msgstr ""
msgid "%{labelStart}Class:%{labelEnd} %{class}"
......@@ -4699,6 +4701,9 @@ msgstr ""
msgid "Closed"
msgstr ""
msgid "Closed %{epicTimeagoDate}"
msgstr ""
msgid "Closed issues"
msgstr ""
......@@ -10952,6 +10957,9 @@ msgstr ""
msgid "Go to environments"
msgstr ""
msgid "Go to epic"
msgstr ""
msgid "Go to file"
msgstr ""
......@@ -12638,6 +12646,9 @@ msgstr ""
msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities"
msgstr ""
msgid "Issues with no epics assigned"
msgstr ""
msgid "Issues, merge requests, pushes, and comments."
msgstr ""
......@@ -15763,6 +15774,9 @@ msgstr ""
msgid "Opened"
msgstr ""
msgid "Opened %{epicTimeagoDate}"
msgstr ""
msgid "Opened MRs"
msgstr ""
......
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