added status restriction
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -63,6 +63,7 @@ export type ProjectBoardSettingsPageProps = {
|
||||
boardColumns: BoardColumnModel[];
|
||||
project: ProjectModel;
|
||||
status?: string | null;
|
||||
ticketStatuses: TicketStatusModel[];
|
||||
};
|
||||
|
||||
export type ProjectGeneralSettingsPageProps = {
|
||||
|
||||
Reference in New Issue
Block a user