added status restriction

This commit is contained in:
2026-04-14 19:07:15 +02:00
parent 9927a91327
commit fb10ecd535
12 changed files with 570 additions and 69 deletions
+34 -19
View File
@@ -67,9 +67,8 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel | null): string => {
};
const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
return (
ticket.ticket_status?.allowed_target_statuses ??
props.ticketStatusOptions
return (ticket.ticket_status?.allowed_target_statuses ?? []).filter(
(ticketStatus) => ticketStatus.id !== ticket.ticket_status_id,
);
};
</script>
@@ -397,10 +396,6 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
</td>
<td v-if="canEditTickets" class="px-4 py-4">
<Form
v-if="
allowedTargetStatuses(ticket)
.length > 0
"
v-bind="
ProjectBoardController.move.form(
{
@@ -419,12 +414,23 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
/>
<select
name="ticket_status_id"
:value="
ticket.ticket_status_id ??
''
:disabled="
allowedTargetStatuses(
ticket,
).length === 0 || processing
"
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]"
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] disabled:cursor-not-allowed disabled:opacity-60"
>
<option
v-if="
allowedTargetStatuses(
ticket,
).length === 0
"
value=""
>
No allowed moves
</option>
<option
v-for="ticketStatus in allowedTargetStatuses(
ticket,
@@ -442,18 +448,27 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
<Button
type="submit"
size="sm"
:disabled="processing"
:disabled="
processing ||
allowedTargetStatuses(
ticket,
).length === 0
"
>
Move
</Button>
<p
v-if="
allowedTargetStatuses(
ticket,
).length === 0
"
class="text-muted-foreground text-xs sm:basis-full"
>
This status does not allow
moving anywhere else.
</p>
</Form>
<span
v-else
class="text-muted-foreground text-sm"
>
No allowed status moves.
</span>
</td>
</tr>
</tbody>
+34 -6
View File
@@ -58,7 +58,9 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel | null): string => {
};
const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
return ticket.ticket_status?.allowed_target_statuses ?? [];
return (ticket.ticket_status?.allowed_target_statuses ?? []).filter(
(ticketStatus) => ticketStatus.id !== ticket.ticket_status_id,
);
};
</script>
@@ -255,12 +257,23 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
<select
:id="`ticket-status-${ticket.id}`"
name="ticket_status_id"
:value="
ticket.ticket_status_id ??
''
:disabled="
allowedTargetStatuses(
ticket,
).length === 0 || processing
"
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]"
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] disabled:cursor-not-allowed disabled:opacity-60"
>
<option
v-if="
allowedTargetStatuses(
ticket,
).length === 0
"
value=""
>
No allowed moves
</option>
<option
v-for="ticketStatus in allowedTargetStatuses(
ticket,
@@ -278,11 +291,26 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
<Button
type="submit"
size="sm"
:disabled="processing"
:disabled="
processing ||
allowedTargetStatuses(
ticket,
).length === 0
"
>
Update status
</Button>
</div>
<p
v-if="
allowedTargetStatuses(ticket)
.length === 0
"
class="text-muted-foreground text-xs"
>
This status does not allow moving
anywhere else.
</p>
</Form>
</CardContent>
</Card>
@@ -22,6 +22,7 @@ import {
repositories as repositorySettings,
} from '@/routes/projects/settings';
import type { BreadcrumbItem, ProjectBoardSettingsPageProps } from '@/types';
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
const props = defineProps<ProjectBoardSettingsPageProps>();
@@ -39,6 +40,40 @@ const breadcrumbs: BreadcrumbItem[] = [
href: boardSettings(props.project.id),
},
];
const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
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 transitionTargetOptions = (
sourceStatus: TicketStatusModel,
): TicketStatusModel[] => {
return props.ticketStatuses.filter(
(ticketStatus) => ticketStatus.id !== sourceStatus.id,
);
};
const statusAllowsTarget = (
sourceStatus: TicketStatusModel,
targetStatus: TicketStatusModel,
): boolean => {
return (
sourceStatus.allowed_target_statuses?.some(
(allowedTargetStatus) => allowedTargetStatus.id === targetStatus.id,
) ?? false
);
};
</script>
<template>
@@ -115,6 +150,123 @@ const breadcrumbs: BreadcrumbItem[] = [
</CardContent>
</Card>
<Card class="gap-0 overflow-hidden">
<CardHeader class="border-b">
<CardTitle>Status moves</CardTitle>
<CardDescription>
Choose exactly where each status is allowed to move
next.
</CardDescription>
</CardHeader>
<CardContent class="p-0">
<div class="overflow-x-auto">
<table
class="divide-border min-w-full divide-y text-sm"
>
<thead
class="bg-muted/40 text-muted-foreground text-left"
>
<tr>
<th class="px-4 py-3 font-medium">
Source status
</th>
<th class="px-4 py-3 font-medium">
Allowed move targets
</th>
<th class="px-4 py-3 font-medium">Save</th>
</tr>
</thead>
<tbody class="divide-border divide-y">
<tr
v-for="sourceStatus in ticketStatuses"
:key="sourceStatus.id"
class="bg-background align-top"
>
<td class="px-4 py-4">
<Badge variant="outline">
{{
ticketStatusLabel(sourceStatus)
}}
</Badge>
</td>
<td class="px-4 py-4">
<Form
:id="`status-transitions-${sourceStatus.id}`"
v-bind="
ProjectBoardSettingsController.updateStatusTransitions.form(
[
project.id,
sourceStatus.id,
],
)
"
class="grid gap-3"
v-slot="{ errors }"
>
<div
v-if="
transitionTargetOptions(
sourceStatus,
).length === 0
"
class="text-muted-foreground"
>
No other statuses exist yet.
</div>
<div
v-else
class="grid gap-2 md:grid-cols-2 xl:grid-cols-3"
>
<label
v-for="targetStatus in transitionTargetOptions(
sourceStatus,
)"
:key="targetStatus.id"
class="border-border flex items-center gap-2 rounded-lg border px-3 py-2"
>
<input
type="checkbox"
name="allowed_ticket_status_ids[]"
:value="targetStatus.id"
:default-checked="
statusAllowsTarget(
sourceStatus,
targetStatus,
)
"
class="border-input text-primary focus:ring-ring size-4 rounded"
/>
<span>{{
ticketStatusLabel(
targetStatus,
)
}}</span>
</label>
</div>
<InputError
:message="
errors.allowed_ticket_status_ids
"
/>
</Form>
</td>
<td class="px-4 py-4">
<Button
type="submit"
variant="outline"
:form="`status-transitions-${sourceStatus.id}`"
>
Save
</Button>
</td>
</tr>
</tbody>
</table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Backlog statuses</CardTitle>
+1
View File
@@ -63,6 +63,7 @@ export type ProjectBoardSettingsPageProps = {
boardColumns: BoardColumnModel[];
project: ProjectModel;
status?: string | null;
ticketStatuses: TicketStatusModel[];
};
export type ProjectGeneralSettingsPageProps = {