Implement ticket statuses
This commit is contained in:
@@ -22,6 +22,8 @@ import { index, show } from '@/routes/projects/index';
|
||||
import { show as showTicket } from '@/routes/projects/tickets';
|
||||
import { metadata as metadataSettings } from '@/routes/projects/settings';
|
||||
import type { BreadcrumbItem, ProjectBacklogPageProps } from '@/types';
|
||||
import type { TicketModel } from '@/types/models/ticket-model';
|
||||
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
||||
|
||||
const props = defineProps<ProjectBacklogPageProps>();
|
||||
|
||||
@@ -41,7 +43,35 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
];
|
||||
|
||||
const hasTicketMetadata =
|
||||
props.ticketTypes.length > 0 && props.ticketPriorities.length > 0;
|
||||
props.ticketTypes.length > 0 &&
|
||||
props.ticketPriorities.length > 0 &&
|
||||
props.backlogTicketStatuses.length > 0;
|
||||
|
||||
const ticketStatusLabel = (ticketStatus: TicketStatusModel | null): string => {
|
||||
if (ticketStatus === null) {
|
||||
return 'Unknown status';
|
||||
}
|
||||
|
||||
if (
|
||||
ticketStatus.board_column === null ||
|
||||
ticketStatus.board_column === undefined
|
||||
) {
|
||||
return ticketStatus.name;
|
||||
}
|
||||
|
||||
if (ticketStatus.board_column.name === ticketStatus.name) {
|
||||
return ticketStatus.name;
|
||||
}
|
||||
|
||||
return `${ticketStatus.board_column.name} / ${ticketStatus.name}`;
|
||||
};
|
||||
|
||||
const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
return (
|
||||
ticket.ticket_status?.allowed_target_statuses ??
|
||||
props.ticketStatusOptions
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -85,7 +115,7 @@ const hasTicketMetadata =
|
||||
<CardTitle>Create ticket</CardTitle>
|
||||
<CardDescription>
|
||||
New tickets receive the next immutable project key and
|
||||
automatically enter backlog.
|
||||
enter the selected backlog status.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -182,6 +212,28 @@ const hasTicketMetadata =
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="ticket_status_id">
|
||||
Status
|
||||
</Label>
|
||||
<select
|
||||
id="ticket_status_id"
|
||||
name="ticket_status_id"
|
||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
||||
>
|
||||
<option
|
||||
v-for="ticketStatus in backlogTicketStatuses"
|
||||
:key="ticketStatus.id"
|
||||
:value="ticketStatus.id"
|
||||
>
|
||||
{{ ticketStatus.name }}
|
||||
</option>
|
||||
</select>
|
||||
<InputError
|
||||
:message="errors.ticket_status_id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="assignee_user_id">
|
||||
Assignee
|
||||
@@ -237,7 +289,7 @@ const hasTicketMetadata =
|
||||
<CardContent class="flex flex-col gap-4 p-6 text-sm">
|
||||
<p class="text-muted-foreground">
|
||||
Ticket creation needs at least one type and one
|
||||
priority.
|
||||
priority, plus one backlog status.
|
||||
</p>
|
||||
<div>
|
||||
<Button as-child variant="outline">
|
||||
@@ -253,8 +305,8 @@ const hasTicketMetadata =
|
||||
<CardHeader class="border-b">
|
||||
<CardTitle>Backlog tickets</CardTitle>
|
||||
<CardDescription>
|
||||
Tickets remain here until they are moved into a board
|
||||
column.
|
||||
Tickets remain here while their status belongs to
|
||||
backlog.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
@@ -277,6 +329,9 @@ const hasTicketMetadata =
|
||||
<th class="px-4 py-3 font-medium">Key</th>
|
||||
<th class="px-4 py-3 font-medium">Title</th>
|
||||
<th class="px-4 py-3 font-medium">Type</th>
|
||||
<th class="px-4 py-3 font-medium">
|
||||
Status
|
||||
</th>
|
||||
<th class="px-4 py-3 font-medium">
|
||||
Priority
|
||||
</th>
|
||||
@@ -284,7 +339,7 @@ const hasTicketMetadata =
|
||||
v-if="canEditTickets"
|
||||
class="px-4 py-3 font-medium"
|
||||
>
|
||||
Move to board
|
||||
Move to status
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -330,12 +385,22 @@ const hasTicketMetadata =
|
||||
<td class="text-muted-foreground px-4 py-4">
|
||||
{{ ticket.ticket_type.name }}
|
||||
</td>
|
||||
<td class="text-muted-foreground px-4 py-4">
|
||||
{{
|
||||
ticketStatusLabel(
|
||||
ticket.ticket_status,
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
<td class="text-muted-foreground px-4 py-4">
|
||||
{{ ticket.ticket_priority.name }}
|
||||
</td>
|
||||
<td v-if="canEditTickets" class="px-4 py-4">
|
||||
<Form
|
||||
v-if="boardColumns.length > 0"
|
||||
v-if="
|
||||
allowedTargetStatuses(ticket)
|
||||
.length > 0
|
||||
"
|
||||
v-bind="
|
||||
ProjectBoardController.move.form(
|
||||
{
|
||||
@@ -353,15 +418,25 @@ const hasTicketMetadata =
|
||||
value="backlog"
|
||||
/>
|
||||
<select
|
||||
name="board_column_id"
|
||||
name="ticket_status_id"
|
||||
:value="
|
||||
ticket.ticket_status_id ??
|
||||
''
|
||||
"
|
||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 min-w-44 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
||||
>
|
||||
<option
|
||||
v-for="boardColumn in boardColumns"
|
||||
:key="boardColumn.id"
|
||||
:value="boardColumn.id"
|
||||
v-for="ticketStatus in allowedTargetStatuses(
|
||||
ticket,
|
||||
)"
|
||||
:key="ticketStatus.id"
|
||||
:value="ticketStatus.id"
|
||||
>
|
||||
{{ boardColumn.name }}
|
||||
{{
|
||||
ticketStatusLabel(
|
||||
ticketStatus,
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
<Button
|
||||
@@ -377,7 +452,7 @@ const hasTicketMetadata =
|
||||
v-else
|
||||
class="text-muted-foreground text-sm"
|
||||
>
|
||||
Add board columns first.
|
||||
No allowed status moves.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -18,6 +18,8 @@ import { board as boardSettings } from '@/routes/projects/settings';
|
||||
import { index as boardIndex } from '@/routes/projects/board';
|
||||
import { show as showTicket } from '@/routes/projects/tickets';
|
||||
import type { BreadcrumbItem, ProjectBoardPageProps } from '@/types';
|
||||
import type { TicketModel } from '@/types/models/ticket-model';
|
||||
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
||||
|
||||
const props = defineProps<ProjectBoardPageProps>();
|
||||
|
||||
@@ -36,16 +38,28 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const statusOptions = [
|
||||
{
|
||||
label: 'Backlog',
|
||||
value: '',
|
||||
},
|
||||
...props.boardColumns.map((boardColumn) => ({
|
||||
label: boardColumn.name,
|
||||
value: String(boardColumn.id),
|
||||
})),
|
||||
];
|
||||
const ticketStatusLabel = (ticketStatus: TicketStatusModel | null): string => {
|
||||
if (ticketStatus === null) {
|
||||
return 'Unknown status';
|
||||
}
|
||||
|
||||
if (
|
||||
ticketStatus.board_column === null ||
|
||||
ticketStatus.board_column === undefined
|
||||
) {
|
||||
return ticketStatus.name;
|
||||
}
|
||||
|
||||
if (ticketStatus.board_column.name === ticketStatus.name) {
|
||||
return ticketStatus.name;
|
||||
}
|
||||
|
||||
return `${ticketStatus.board_column.name} / ${ticketStatus.name}`;
|
||||
};
|
||||
|
||||
const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
return ticket.ticket_status?.allowed_target_statuses ?? [];
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -116,6 +130,21 @@ const statusOptions = [
|
||||
{{ boardColumn.tickets?.length ?? 0 }}
|
||||
active ticket(s)
|
||||
</CardDescription>
|
||||
<div
|
||||
v-if="
|
||||
(boardColumn.ticket_statuses?.length ??
|
||||
0) > 0
|
||||
"
|
||||
class="mt-3 flex flex-wrap gap-2"
|
||||
>
|
||||
<Badge
|
||||
v-for="ticketStatus in boardColumn.ticket_statuses"
|
||||
:key="ticketStatus.id"
|
||||
variant="outline"
|
||||
>
|
||||
{{ ticketStatus.name }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Badge variant="secondary">
|
||||
@@ -160,6 +189,13 @@ const statusOptions = [
|
||||
<Badge variant="secondary">
|
||||
{{ ticket.ticket_priority.name }}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{{
|
||||
ticketStatusLabel(
|
||||
ticket.ticket_status,
|
||||
)
|
||||
}}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle class="text-base">
|
||||
<Link
|
||||
@@ -218,18 +254,25 @@ const statusOptions = [
|
||||
>
|
||||
<select
|
||||
:id="`ticket-status-${ticket.id}`"
|
||||
name="board_column_id"
|
||||
name="ticket_status_id"
|
||||
:value="
|
||||
ticket.board_column_id ?? ''
|
||||
ticket.ticket_status_id ??
|
||||
''
|
||||
"
|
||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 min-w-44 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
||||
>
|
||||
<option
|
||||
v-for="statusOption in statusOptions"
|
||||
:key="statusOption.label"
|
||||
:value="statusOption.value"
|
||||
v-for="ticketStatus in allowedTargetStatuses(
|
||||
ticket,
|
||||
)"
|
||||
:key="ticketStatus.id"
|
||||
:value="ticketStatus.id"
|
||||
>
|
||||
{{ statusOption.label }}
|
||||
{{
|
||||
ticketStatusLabel(
|
||||
ticketStatus,
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
<Button
|
||||
|
||||
@@ -354,6 +354,9 @@ const formatDateTime = (value: string): string => {
|
||||
<Badge variant="secondary">
|
||||
{{ ticket.ticket_priority.name }}
|
||||
</Badge>
|
||||
<Badge v-if="ticket.ticket_status" variant="outline">
|
||||
{{ ticket.ticket_status.name }}
|
||||
</Badge>
|
||||
<Badge v-if="!canEditTicket" variant="outline">
|
||||
Read only
|
||||
</Badge>
|
||||
|
||||
@@ -93,20 +93,135 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
<CardHeader>
|
||||
<CardTitle>Workflow model</CardTitle>
|
||||
<CardDescription>
|
||||
Backlog is always the intake queue and stays outside the
|
||||
normal board column list.
|
||||
Backlog is always the intake queue; board columns group
|
||||
the statuses that appear on the board.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="flex flex-wrap gap-3">
|
||||
<Badge class="px-3 py-1">Backlog</Badge>
|
||||
<Badge
|
||||
v-for="ticketStatus in backlogTicketStatuses"
|
||||
:key="ticketStatus.id"
|
||||
variant="outline"
|
||||
class="px-3 py-1"
|
||||
>
|
||||
{{ ticketStatus.name }}
|
||||
</Badge>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Tickets will later be valid only in backlog or in one of
|
||||
the ordered columns below.
|
||||
Tickets move between statuses; backlog statuses are not
|
||||
included in a board column.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Backlog statuses</CardTitle>
|
||||
<CardDescription>
|
||||
New backlog tickets choose one of these statuses.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="space-y-4">
|
||||
<div
|
||||
v-for="ticketStatus in backlogTicketStatuses"
|
||||
:key="ticketStatus.id"
|
||||
class="border-border rounded-lg border p-4"
|
||||
>
|
||||
<Form
|
||||
v-bind="
|
||||
ProjectBoardSettingsController.updateStatus.form(
|
||||
[project.id, ticketStatus.id],
|
||||
)
|
||||
"
|
||||
class="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]"
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
:for="`backlog-status-${ticketStatus.id}`"
|
||||
>
|
||||
Status name
|
||||
</Label>
|
||||
<Input
|
||||
:id="`backlog-status-${ticketStatus.id}`"
|
||||
name="name"
|
||||
:default-value="ticketStatus.name"
|
||||
required
|
||||
/>
|
||||
<InputError :message="errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-end">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
:disabled="processing"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div class="mt-3 flex justify-end">
|
||||
<Form
|
||||
v-bind="
|
||||
ProjectBoardSettingsController.destroyStatus.form(
|
||||
[project.id, ticketStatus.id],
|
||||
)
|
||||
"
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<InputError
|
||||
:message="errors.ticket_status_id"
|
||||
class="mb-2"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
:disabled="
|
||||
processing ||
|
||||
backlogTicketStatuses.length <= 1
|
||||
"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
v-bind="
|
||||
ProjectBoardSettingsController.storeStatus.form(
|
||||
project.id,
|
||||
)
|
||||
"
|
||||
class="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]"
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<Label for="backlog-status-name">
|
||||
New backlog status
|
||||
</Label>
|
||||
<Input
|
||||
id="backlog-status-name"
|
||||
name="name"
|
||||
placeholder="Ready"
|
||||
required
|
||||
/>
|
||||
<InputError :message="errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-end">
|
||||
<Button type="submit" :disabled="processing">
|
||||
Add status
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="grid gap-6 xl:grid-cols-[22rem_minmax(0,1fr)]">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -239,6 +354,140 @@ const breadcrumbs: BreadcrumbItem[] = [
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div class="border-border mt-4 border-t pt-4">
|
||||
<div class="mb-3">
|
||||
<p class="text-sm font-medium">
|
||||
Ticket statuses
|
||||
</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
Moving into this column requires a
|
||||
specific status.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="ticketStatus in column.ticket_statuses ??
|
||||
[]"
|
||||
:key="ticketStatus.id"
|
||||
class="border-border rounded-lg border p-3"
|
||||
>
|
||||
<Form
|
||||
v-bind="
|
||||
ProjectBoardSettingsController.updateStatus.form(
|
||||
[
|
||||
project.id,
|
||||
ticketStatus.id,
|
||||
],
|
||||
)
|
||||
"
|
||||
class="grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]"
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
:for="`column-status-${ticketStatus.id}`"
|
||||
>
|
||||
Status name
|
||||
</Label>
|
||||
<Input
|
||||
:id="`column-status-${ticketStatus.id}`"
|
||||
name="name"
|
||||
:default-value="
|
||||
ticketStatus.name
|
||||
"
|
||||
required
|
||||
/>
|
||||
<InputError
|
||||
:message="errors.name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
:disabled="processing"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div class="mt-3 flex justify-end">
|
||||
<Form
|
||||
v-bind="
|
||||
ProjectBoardSettingsController.destroyStatus.form(
|
||||
[
|
||||
project.id,
|
||||
ticketStatus.id,
|
||||
],
|
||||
)
|
||||
"
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<InputError
|
||||
:message="
|
||||
errors.ticket_status_id
|
||||
"
|
||||
class="mb-2"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
:disabled="
|
||||
processing ||
|
||||
(column.ticket_statuses
|
||||
?.length ?? 0) <= 1
|
||||
"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
v-bind="
|
||||
ProjectBoardSettingsController.storeStatus.form(
|
||||
project.id,
|
||||
)
|
||||
"
|
||||
class="mt-4 grid gap-4 md:grid-cols-[minmax(0,1fr)_auto]"
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="board_column_id"
|
||||
:value="column.id"
|
||||
/>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
:for="`new-column-status-${column.id}`"
|
||||
>
|
||||
New status
|
||||
</Label>
|
||||
<Input
|
||||
:id="`new-column-status-${column.id}`"
|
||||
name="name"
|
||||
placeholder="Ready for QA"
|
||||
required
|
||||
/>
|
||||
<InputError :message="errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-end">
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
Add status
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -205,7 +205,7 @@ const submitUpdate = (): void => {
|
||||
updateForm.submit(
|
||||
ProjectRepositorySettingsController.update([
|
||||
props.project.id,
|
||||
updateForm.repository_connection_id,
|
||||
Number(updateForm.repository_connection_id),
|
||||
]),
|
||||
{
|
||||
preserveScroll: true,
|
||||
|
||||
Reference in New Issue
Block a user