166 lines
6.9 KiB
Vue
166 lines
6.9 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue';
|
|
import { UserAvatar } from '@/components/ui/avatar';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import { ticketStatusLabel } from '@/lib/tickets';
|
|
import type { TicketDetailMetadataProps } from '@/types/ticket-detail';
|
|
const props = defineProps<TicketDetailMetadataProps>();
|
|
const { t, date, number } = useLocalization();
|
|
const fields = computed(() =>
|
|
props.ticketFieldDefinitions.map((definition) => {
|
|
const value = props.ticket.field_values?.find(
|
|
(field) => field.ticket_field_definition_id === definition.id,
|
|
);
|
|
return {
|
|
id: definition.id,
|
|
label: definition.label,
|
|
value:
|
|
definition.field_type === 'user'
|
|
? (value?.user_value?.name ?? t('tickets.unassigned'))
|
|
: value?.value_number != null
|
|
? number(value.value_number)
|
|
: t('ticketDetail.notSet'),
|
|
};
|
|
}),
|
|
);
|
|
</script>
|
|
<template>
|
|
<section
|
|
id="ticket-metadata"
|
|
aria-labelledby="ticket-metadata-title"
|
|
class="min-w-0 scroll-mt-6"
|
|
>
|
|
<Card class="bg-muted/30">
|
|
<CardHeader
|
|
><h2 id="ticket-metadata-title" class="text-lg font-semibold">
|
|
{{ t('ticketDetail.metadata') }}
|
|
</h2></CardHeader
|
|
>
|
|
<CardContent class="wrap-anywhere flex min-w-0 flex-col gap-6">
|
|
<dl
|
|
class="grid min-w-0 gap-5 text-sm sm:grid-cols-2 xl:grid-cols-1"
|
|
>
|
|
<div class="space-y-2">
|
|
<dt class="text-muted-foreground">
|
|
{{ t('ticketDetail.status') }}
|
|
</dt>
|
|
<dd>
|
|
{{
|
|
ticketStatusLabel(
|
|
ticket.ticket_status,
|
|
t('tickets.noStatus'),
|
|
)
|
|
}}
|
|
</dd>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<dt class="text-muted-foreground">
|
|
{{ t('ticketDetail.type') }}
|
|
</dt>
|
|
<dd>{{ ticket.ticket_type.name }}</dd>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<dt class="text-muted-foreground">
|
|
{{ t('ticketDetail.priority') }}
|
|
</dt>
|
|
<dd>
|
|
<Badge variant="outline">{{
|
|
ticket.ticket_priority.name
|
|
}}</Badge>
|
|
</dd>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<dt class="text-muted-foreground">
|
|
{{ t('ticketDetail.assignee') }}
|
|
</dt>
|
|
<dd class="flex items-center gap-2">
|
|
<UserAvatar
|
|
v-if="ticket.assignee"
|
|
:user="ticket.assignee"
|
|
size="sm"
|
|
/>{{
|
|
ticket.assignee?.name ?? t('tickets.unassigned')
|
|
}}
|
|
</dd>
|
|
</div>
|
|
<div
|
|
v-if="project.show_author || canManageProject"
|
|
class="space-y-2"
|
|
>
|
|
<dt class="text-muted-foreground">
|
|
{{ t('ticketDetail.author') }}
|
|
</dt>
|
|
<dd class="flex items-center gap-2">
|
|
<UserAvatar
|
|
v-if="ticket.author"
|
|
:user="ticket.author"
|
|
size="sm"
|
|
/>{{
|
|
ticket.author?.name ??
|
|
t('ticketDetail.noAuthor')
|
|
}}
|
|
</dd>
|
|
</div>
|
|
<div v-if="project.show_story_points" class="space-y-2">
|
|
<dt class="text-muted-foreground">
|
|
{{ t('ticketDetail.storyPoints') }}
|
|
</dt>
|
|
<dd>
|
|
{{
|
|
ticket.story_points != null
|
|
? number(ticket.story_points)
|
|
: t('ticketDetail.notSet')
|
|
}}
|
|
</dd>
|
|
</div>
|
|
<div
|
|
v-for="field in fields"
|
|
:key="field.id"
|
|
class="space-y-2"
|
|
>
|
|
<dt class="text-muted-foreground">{{ field.label }}</dt>
|
|
<dd>{{ field.value }}</dd>
|
|
</div>
|
|
</dl>
|
|
<div
|
|
v-if="ticket.is_blocked"
|
|
class="border-warning/30 bg-warning/10 space-y-2 rounded-lg border p-4 text-sm"
|
|
>
|
|
<p class="font-semibold">{{ t('tickets.blocked') }}</p>
|
|
<p class="whitespace-pre-wrap">
|
|
{{ ticket.blocked_comment }}
|
|
</p>
|
|
</div>
|
|
<dl
|
|
class="text-muted-foreground grid gap-4 border-t pt-5 text-xs"
|
|
>
|
|
<div class="space-y-1">
|
|
<dt>{{ t('ticketDetail.created') }}</dt>
|
|
<dd>
|
|
<time :datetime="ticket.created_at">{{
|
|
date(new Date(ticket.created_at), {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
})
|
|
}}</time>
|
|
</dd>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<dt>{{ t('ticketDetail.updated') }}</dt>
|
|
<dd>
|
|
<time :datetime="ticket.updated_at">{{
|
|
date(new Date(ticket.updated_at), {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
})
|
|
}}</time>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</template>
|