Implement parent child ticket hierarchy
This commit is contained in:
@@ -3,6 +3,7 @@ import { useForm, Head, Link } from '@inertiajs/vue3';
|
||||
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
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 {
|
||||
@@ -57,6 +58,7 @@ const form = useForm({
|
||||
value_user_id: currentFieldValue?.value_user_id ?? '',
|
||||
};
|
||||
}),
|
||||
parent_ticket_id: props.ticket.parent_ticket_id ?? '',
|
||||
ticket_priority_id: props.ticket.ticket_priority_id,
|
||||
ticket_type_id: props.ticket.ticket_type_id,
|
||||
title: props.ticket.title,
|
||||
@@ -220,6 +222,34 @@ const formatDateTime = (value: string): string => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="parent_ticket_id">
|
||||
Parent ticket
|
||||
</Label>
|
||||
<select
|
||||
id="parent_ticket_id"
|
||||
v-model="form.parent_ticket_id"
|
||||
: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"
|
||||
>
|
||||
<option value="">No parent ticket</option>
|
||||
<option
|
||||
v-for="parentTicketOption in parentTicketOptions"
|
||||
:key="parentTicketOption.id"
|
||||
:value="parentTicketOption.id"
|
||||
>
|
||||
{{
|
||||
`${parentTicketOption.ticket_key} - ${parentTicketOption.title}`
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
<InputError
|
||||
:message="form.errors.parent_ticket_id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="description">Description</Label>
|
||||
<textarea
|
||||
@@ -357,53 +387,90 @@ const formatDateTime = (value: string): string => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Worklog</CardTitle>
|
||||
<CardDescription>
|
||||
Every tracked ticket change is recorded with actor
|
||||
and timestamp.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<div class="space-y-6">
|
||||
<Card class="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Child tickets</CardTitle>
|
||||
<CardDescription>
|
||||
Parent tickets expose their full child tree
|
||||
here, including nested parent chains.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-4">
|
||||
<div
|
||||
v-if="(ticket.worklog_entries?.length ?? 0) === 0"
|
||||
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
|
||||
>
|
||||
No worklog entries exist for this ticket yet.
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<CardContent>
|
||||
<div
|
||||
v-for="worklogEntry in ticket.worklog_entries"
|
||||
:key="worklogEntry.id"
|
||||
class="rounded-xl border border-border px-4 py-4"
|
||||
v-if="childTicketTree.length === 0"
|
||||
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="outline">
|
||||
{{ worklogEntry.entry_type }}
|
||||
</Badge>
|
||||
<span
|
||||
class="text-sm font-medium text-foreground"
|
||||
No child tickets are assigned to this ticket
|
||||
yet.
|
||||
</div>
|
||||
|
||||
<TicketTreeList
|
||||
v-else
|
||||
:nodes="childTicketTree"
|
||||
:project-id="project.id"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card class="h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Worklog</CardTitle>
|
||||
<CardDescription>
|
||||
Every tracked ticket change is recorded with
|
||||
actor and timestamp.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-4">
|
||||
<div
|
||||
v-if="
|
||||
(ticket.worklog_entries?.length ?? 0) === 0
|
||||
"
|
||||
class="rounded-xl border border-dashed border-border px-4 py-6 text-sm text-muted-foreground"
|
||||
>
|
||||
No worklog entries exist for this ticket yet.
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<div
|
||||
v-for="worklogEntry in ticket.worklog_entries"
|
||||
:key="worklogEntry.id"
|
||||
class="rounded-xl border border-border px-4 py-4"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
>
|
||||
<Badge variant="outline">
|
||||
{{ worklogEntry.entry_type }}
|
||||
</Badge>
|
||||
<span
|
||||
class="text-sm font-medium text-foreground"
|
||||
>
|
||||
{{
|
||||
worklogEntry.actor?.name ??
|
||||
'System'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-foreground">
|
||||
{{ worklogEntry.summary }}
|
||||
</p>
|
||||
<p
|
||||
class="mt-2 text-xs text-muted-foreground"
|
||||
>
|
||||
{{
|
||||
worklogEntry.actor?.name ?? 'System'
|
||||
formatDateTime(
|
||||
worklogEntry.created_at,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-foreground">
|
||||
{{ worklogEntry.summary }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
{{
|
||||
formatDateTime(worklogEntry.created_at)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
|
||||
Reference in New Issue
Block a user