Implement ticket links and blocker state
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user