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