224 lines
9.3 KiB
Vue
224 lines
9.3 KiB
Vue
<script setup lang="ts">
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
|
|
import { Form, Link } from '@inertiajs/vue3';
|
|
import { computed } from 'vue';
|
|
import ProjectTicketLinkController from '@/actions/App/Http/Controllers/ProjectTicketLinkController';
|
|
import InputError from '@/components/InputError.vue';
|
|
import TicketTreeList from '@/components/TicketTreeList.vue';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { show as showTicket } from '@/routes/projects/tickets';
|
|
import type { TicketDetailRelationshipsProps } from '@/types/ticket-detail';
|
|
const props = defineProps<TicketDetailRelationshipsProps>();
|
|
const { t } = useLocalization();
|
|
const currentParentTicket = computed(
|
|
() =>
|
|
props.ticket.parent_ticket ??
|
|
props.parentTicketOptions.find(
|
|
(parentTicket) => parentTicket.id === props.ticket.parent_ticket_id,
|
|
) ??
|
|
null,
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
id="ticket-relationships"
|
|
aria-labelledby="ticket-relationships-title"
|
|
class="wrap-anywhere flex min-w-0 scroll-mt-6 flex-col gap-4"
|
|
>
|
|
<h2 id="ticket-relationships-title" class="text-lg font-semibold">
|
|
{{ t('ticketDetail.relationships') }}
|
|
</h2>
|
|
<Card v-if="ticket.ticket_type?.is_parent" class="h-fit">
|
|
<CardHeader>
|
|
<CardTitle>{{ t('ticketDetail.children') }}</CardTitle>
|
|
<CardDescription>{{
|
|
t('ticketDetail.childrenDescription')
|
|
}}</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<div
|
|
v-if="childTicketTree.length === 0"
|
|
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
|
>
|
|
{{ t('ticketDetail.noChildren') }}
|
|
</div>
|
|
|
|
<TicketTreeList
|
|
v-else
|
|
:nodes="childTicketTree"
|
|
:project-id="project.id"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card v-else-if="currentParentTicket !== null" class="h-fit">
|
|
<CardHeader>
|
|
<CardTitle>{{ t('ticketDetail.parent') }}</CardTitle>
|
|
<CardDescription>{{
|
|
t('ticketDetail.parentDescription')
|
|
}}</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<Link
|
|
:href="
|
|
showTicket({
|
|
project: project.id,
|
|
ticket: currentParentTicket.id,
|
|
})
|
|
"
|
|
class="border-border hover:border-primary/60 block rounded-xl border px-4 py-4 transition"
|
|
>
|
|
<div class="font-medium">
|
|
{{ currentParentTicket.ticket_key }}
|
|
</div>
|
|
<div class="text-muted-foreground mt-1 text-sm">
|
|
{{ currentParentTicket.title }}
|
|
</div>
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card class="h-fit">
|
|
<CardHeader>
|
|
<CardTitle>{{ t('ticketDetail.linkedTickets') }}</CardTitle>
|
|
<CardDescription>{{
|
|
t('ticketDetail.linkedTicketsDescription')
|
|
}}</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent class="space-y-4">
|
|
<Form
|
|
v-if="
|
|
canEditTicket &&
|
|
ticketLinkTypes.length > 0 &&
|
|
ticketLinkOptions.length > 0
|
|
"
|
|
v-bind="
|
|
ProjectTicketLinkController.store.form({
|
|
project: project.id,
|
|
ticket: ticket.id,
|
|
})
|
|
"
|
|
class="border-border grid gap-4 rounded-xl border border-dashed p-4"
|
|
v-slot="{ errors, processing }"
|
|
>
|
|
<div class="grid min-w-0 gap-2">
|
|
<Label for="ticket_link_type_id">{{
|
|
t('ticketDetail.linkType')
|
|
}}</Label>
|
|
<select
|
|
id="ticket_link_type_id"
|
|
name="ticket_link_type_id"
|
|
class="min-h-control border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 w-full min-w-0 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
>
|
|
<option
|
|
v-for="ticketLinkType in ticketLinkTypes"
|
|
:key="ticketLinkType.id"
|
|
:value="ticketLinkType.id"
|
|
>
|
|
{{ ticketLinkType.label }}
|
|
</option>
|
|
</select>
|
|
<InputError :message="errors.ticket_link_type_id" />
|
|
</div>
|
|
|
|
<div class="grid min-w-0 gap-2">
|
|
<Label for="linked_ticket_id">{{
|
|
t('ticketDetail.linkedTicket')
|
|
}}</Label>
|
|
<select
|
|
id="linked_ticket_id"
|
|
name="linked_ticket_id"
|
|
class="min-h-control border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 w-full min-w-0 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
>
|
|
<option
|
|
v-for="ticketLinkOption in ticketLinkOptions"
|
|
:key="ticketLinkOption.id"
|
|
:value="ticketLinkOption.id"
|
|
>
|
|
{{
|
|
`${ticketLinkOption.ticket_key} - ${ticketLinkOption.title}`
|
|
}}
|
|
</option>
|
|
</select>
|
|
<InputError :message="errors.linked_ticket_id" />
|
|
</div>
|
|
|
|
<Button type="submit" :disabled="processing">{{
|
|
t('ticketDetail.saveLink')
|
|
}}</Button>
|
|
</Form>
|
|
|
|
<div
|
|
v-if="linkedTickets.length === 0"
|
|
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
|
>
|
|
{{ t('ticketDetail.noLinks') }}
|
|
</div>
|
|
|
|
<div v-else class="space-y-3">
|
|
<div
|
|
v-for="linkedTicket in linkedTickets"
|
|
:key="linkedTicket.id"
|
|
class="border-border rounded-xl border px-4 py-4"
|
|
>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<Badge variant="secondary">
|
|
{{ linkedTicket.ticket_link_type.label }}
|
|
</Badge>
|
|
<Link
|
|
:href="
|
|
showTicket({
|
|
project: project.id,
|
|
ticket: linkedTicket.linked_ticket.id,
|
|
})
|
|
"
|
|
class="text-foreground hover:text-primary font-medium transition"
|
|
>
|
|
{{ linkedTicket.linked_ticket.ticket_key }}
|
|
</Link>
|
|
</div>
|
|
<p class="text-muted-foreground mt-2 text-sm">
|
|
{{ linkedTicket.linked_ticket.title }}
|
|
</p>
|
|
|
|
<Form
|
|
v-if="canEditTicket"
|
|
v-bind="
|
|
ProjectTicketLinkController.destroy.form({
|
|
project: project.id,
|
|
ticket: ticket.id,
|
|
ticketLink: linkedTicket.id,
|
|
})
|
|
"
|
|
class="mt-3 flex justify-end"
|
|
v-slot="{ processing: deleting }"
|
|
>
|
|
<Button
|
|
type="submit"
|
|
variant="destructive"
|
|
size="sm"
|
|
:disabled="deleting"
|
|
>{{ t('ticketDetail.removeLink') }}</Button
|
|
>
|
|
</Form>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</template>
|