Implement ticket links and blocker state

This commit is contained in:
2026-03-25 09:12:17 +01:00
parent 3b31e6bb7d
commit 70b90f32d4
34 changed files with 1722 additions and 10 deletions
+189 -1
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useForm, Head, Link } from '@inertiajs/vue3';
import { Form, useForm, Head, Link } from '@inertiajs/vue3';
import ProjectTicketLinkController from '@/actions/App/Http/Controllers/ProjectTicketLinkController';
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
import Heading from '@/components/Heading.vue';
import InputError from '@/components/InputError.vue';
@@ -44,6 +45,7 @@ const breadcrumbs: BreadcrumbItem[] = [
const form = useForm({
acceptance_criteria: props.ticket.acceptance_criteria ?? '',
blocked_comment: props.ticket.blocked_comment ?? '',
description: props.ticket.description ?? '',
field_values: props.ticketFieldDefinitions.map((ticketFieldDefinition) => {
const currentFieldValue = props.ticket.field_values?.find(
@@ -58,6 +60,7 @@ const form = useForm({
value_user_id: currentFieldValue?.value_user_id ?? '',
};
}),
is_blocked: props.ticket.is_blocked,
parent_ticket_id: props.ticket.parent_ticket_id ?? '',
ticket_priority_id: props.ticket.ticket_priority_id,
ticket_type_id: props.ticket.ticket_type_id,
@@ -284,6 +287,42 @@ const formatDateTime = (value: string): string => {
/>
</div>
<div class="grid gap-3 rounded-xl border border-border p-4">
<div class="flex items-center gap-3">
<input
id="is_blocked"
v-model="form.is_blocked"
type="checkbox"
:disabled="
!canEditTicket || form.processing
"
class="size-4 rounded border-input text-primary focus:ring-ring"
/>
<Label for="is_blocked">
Mark ticket as blocked
</Label>
</div>
<div class="grid gap-2">
<Label for="blocked_comment">
Blocker comment
</Label>
<textarea
id="blocked_comment"
v-model="form.blocked_comment"
rows="3"
:disabled="
!canEditTicket || form.processing
"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-60"
placeholder="Describe what currently blocks progress on this ticket."
/>
<InputError
:message="form.errors.blocked_comment"
/>
</div>
</div>
<div class="grid gap-4">
<div class="space-y-1">
<h3 class="text-sm font-semibold">
@@ -388,6 +427,155 @@ const formatDateTime = (value: string): string => {
</Card>
<div class="space-y-6">
<Card class="h-fit">
<CardHeader>
<CardTitle>Linked tickets</CardTitle>
<CardDescription>
Track cross-ticket relationships with
project-defined link labels.
</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="grid gap-4 rounded-xl border border-dashed border-border p-4"
v-slot="{ errors, processing }"
>
<div class="grid gap-2">
<Label for="ticket_link_type_id">
Link type
</Label>
<select
id="ticket_link_type_id"
name="ticket_link_type_id"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<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 gap-2">
<Label for="linked_ticket_id">
Linked ticket
</Label>
<select
id="linked_ticket_id"
name="linked_ticket_id"
class="rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<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"
>
Save link
</Button>
</Form>
<div
v-if="linkedTickets.length === 0"
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
>
No ticket links are configured for this ticket
yet.
</div>
<div v-else class="space-y-3">
<div
v-for="linkedTicket in linkedTickets"
:key="linkedTicket.id"
class="rounded-xl border border-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="font-medium text-foreground transition hover:text-primary"
>
{{
linkedTicket.linked_ticket
.ticket_key
}}
</Link>
</div>
<p class="mt-2 text-sm text-muted-foreground">
{{ 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"
>
Remove
</Button>
</Form>
</div>
</div>
</CardContent>
</Card>
<Card class="h-fit">
<CardHeader>
<CardTitle>Child tickets</CardTitle>