diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/issue')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue | 94 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue | 90 |
2 files changed, 184 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue new file mode 100644 index 00000000000..7e79e63aa1e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue @@ -0,0 +1,94 @@ +<script> +import { GlTooltipDirective } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; + +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + +export default { + components: { + UserAvatarLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + assignees: { + type: Array, + required: true, + }, + }, + data() { + return { + maxVisibleAssignees: 2, + maxAssigneeAvatars: 3, + maxAssignees: 99, + }; + }, + computed: { + countOverLimit() { + return this.assignees.length - this.maxVisibleAssignees; + }, + assigneesToShow() { + if (this.assignees.length > this.maxAssigneeAvatars) { + return this.assignees.slice(0, this.maxVisibleAssignees); + } + return this.assignees; + }, + assigneesCounterTooltip() { + const { countOverLimit, maxAssignees } = this; + const count = countOverLimit > maxAssignees ? maxAssignees : countOverLimit; + + return sprintf(__('%{count} more assignees'), { count }); + }, + shouldRenderAssigneesCounter() { + const assigneesCount = this.assignees.length; + if (assigneesCount <= this.maxAssigneeAvatars) { + return false; + } + + return assigneesCount > this.countOverLimit; + }, + assigneeCounterLabel() { + if (this.countOverLimit > this.maxAssignees) { + return `${this.maxAssignees}+`; + } + + return `+${this.countOverLimit}`; + }, + }, + methods: { + avatarUrlTitle(assignee) { + return sprintf(__('Avatar for %{assigneeName}'), { + assigneeName: assignee.name, + }); + }, + }, +}; +</script> +<template> + <div class="issue-assignees"> + <user-avatar-link + v-for="assignee in assigneesToShow" + :key="assignee.id" + :link-href="assignee.web_url" + :img-alt="avatarUrlTitle(assignee)" + :img-src="assignee.avatar_url" + :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="shouldRenderAssigneesCounter" + v-gl-tooltip + :title="assigneesCounterTooltip" + class="avatar-counter" + data-placement="bottom" + >{{ assigneeCounterLabel }}</span + > + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue new file mode 100644 index 00000000000..d5d967e25bf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/issue/issue_milestone.vue @@ -0,0 +1,90 @@ +<script> +import { GlTooltip } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + GlTooltip, + }, + mixins: [timeagoMixin], + props: { + milestone: { + type: Object, + required: true, + }, + }, + data() { + return { + milestoneDue: this.milestone.due_date ? parsePikadayDate(this.milestone.due_date) : null, + milestoneStart: this.milestone.start_date + ? parsePikadayDate(this.milestone.start_date) + : null, + }; + }, + computed: { + isMilestoneStarted() { + if (!this.milestoneStart) { + return false; + } + return Date.now() > this.milestoneStart; + }, + isMilestonePastDue() { + if (!this.milestoneDue) { + return false; + } + return Date.now() > this.milestoneDue; + }, + milestoneDatesAbsolute() { + if (this.milestoneDue) { + return `(${dateInWords(this.milestoneDue)})`; + } else if (this.milestoneStart) { + return `(${dateInWords(this.milestoneStart)})`; + } + return ''; + }, + milestoneDatesHuman() { + if (this.milestoneStart || this.milestoneDue) { + if (this.milestoneDue) { + return timeFor( + this.milestoneDue, + sprintf(__('Expired %{expiredOn}'), { + expiredOn: this.timeFormated(this.milestoneDue), + }), + ); + } + + return sprintf( + this.isMilestoneStarted ? __('Started %{startsIn}') : __('Starts %{startsIn}'), + { + startsIn: this.timeFormated(this.milestoneStart), + }, + ); + } + return ''; + }, + }, +}; +</script> +<template> + <div ref="milestoneDetails" class="issue-milestone-details"> + <icon :size="16" class="inline icon" name="clock" /> + <span class="milestone-title">{{ milestone.title }}</span> + <gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone"> + <span class="bold">{{ __('Milestone') }}</span> <br /> + <span>{{ milestone.title }}</span> <br /> + <span + v-if="milestoneStart || milestoneDue" + :class="{ + 'text-danger-muted': isMilestonePastDue, + 'text-tertiary': !isMilestonePastDue, + }" + ><span>{{ milestoneDatesHuman }}</span + ><br /><span>{{ milestoneDatesAbsolute }}</span> + </span> + </gl-tooltip> + </div> +</template> |