205 lines
7.8 KiB
Vue
205 lines
7.8 KiB
Vue
<script setup lang="ts">
|
|
import { useForm } from '@inertiajs/vue3';
|
|
import { computed } from 'vue';
|
|
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
|
|
import FormFeedback from '@/components/FormFeedback.vue';
|
|
import InputError from '@/components/InputError.vue';
|
|
import TicketFormField from '@/components/tickets/TicketFormField.vue';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
} from '@/components/ui/card';
|
|
import { Label } from '@/components/ui/label';
|
|
import { useFormProtection } from '@/composables/useFormProtection';
|
|
import { useLocalization } from '@/composables/useLocalization';
|
|
import type { TicketDetailBlockerProps } from '@/types/ticket-detail';
|
|
|
|
const props = defineProps<TicketDetailBlockerProps>();
|
|
const { t } = useLocalization();
|
|
|
|
const savedFormData = () => ({
|
|
acceptance_criteria: props.ticket.acceptance_criteria,
|
|
assignee_user_id: props.ticket.assignee_user_id,
|
|
blocked_comment: props.ticket.blocked_comment ?? '',
|
|
description: props.ticket.description,
|
|
field_values: (props.ticket.field_values ?? []).map((fieldValue) => ({
|
|
ticket_field_definition_id: fieldValue.ticket_field_definition_id,
|
|
value_number: fieldValue.value_number,
|
|
value_user_id: fieldValue.value_user_id,
|
|
})),
|
|
is_blocked: props.ticket.is_blocked,
|
|
parent_ticket_id: props.ticket.parent_ticket_id,
|
|
story_points: props.ticket.story_points,
|
|
ticket_priority_id: props.ticket.ticket_priority_id,
|
|
ticket_type_id: props.ticket.ticket_type_id,
|
|
title: props.ticket.title,
|
|
});
|
|
|
|
const form = useForm(
|
|
`projects:${props.project.id}:tickets:${props.ticket.id}:blocker`,
|
|
savedFormData(),
|
|
);
|
|
const protection = useFormProtection(form);
|
|
const isBlocked = computed(() => Boolean(form.is_blocked));
|
|
|
|
const reset = (): void => {
|
|
if (form.processing || !protection.confirmLeave()) {
|
|
return;
|
|
}
|
|
|
|
form.reset();
|
|
form.clearErrors();
|
|
protection.clearFeedback();
|
|
};
|
|
|
|
const submit = (): void => {
|
|
if (!props.canEditTicket || form.processing) {
|
|
return;
|
|
}
|
|
|
|
protection.submit(
|
|
ProjectTicketController.update({
|
|
project: props.project.id,
|
|
ticket: props.ticket.id,
|
|
}),
|
|
{
|
|
preserveScroll: true,
|
|
onSuccess: () => {
|
|
Object.assign(form, savedFormData());
|
|
form.defaults();
|
|
},
|
|
},
|
|
);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<section
|
|
id="ticket-blocker"
|
|
aria-labelledby="ticket-blocker-title"
|
|
class="min-w-0 scroll-mt-6"
|
|
>
|
|
<Card :class="{ 'border-warning/40': isBlocked }">
|
|
<CardHeader>
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<h2 id="ticket-blocker-title" class="text-lg font-semibold">
|
|
{{ t('ticketDetail.blocker') }}
|
|
</h2>
|
|
<Badge :variant="isBlocked ? 'warning' : 'outline'">
|
|
{{
|
|
isBlocked
|
|
? t('ticketDetail.blockerActive')
|
|
: t('ticketDetail.blockerInactive')
|
|
}}
|
|
</Badge>
|
|
</div>
|
|
<CardDescription>
|
|
{{ t('ticketDetail.blockerDescription') }}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent v-if="canEditTicket">
|
|
<form class="grid min-w-0 gap-5" @submit.prevent="submit">
|
|
<FormFeedback
|
|
:state="protection.state.value"
|
|
@retry="submit"
|
|
/>
|
|
|
|
<div
|
|
class="bg-muted/30 flex min-w-0 items-start gap-3 rounded-xl border p-4"
|
|
>
|
|
<input
|
|
id="is_blocked"
|
|
v-model="form.is_blocked"
|
|
type="checkbox"
|
|
:disabled="form.processing"
|
|
:aria-invalid="Boolean(form.errors.is_blocked)"
|
|
aria-describedby="ticket-blocker-state-description is_blocked-error"
|
|
class="border-input text-primary focus-visible:ring-ring mt-0.5 size-4 shrink-0 rounded focus-visible:ring-2"
|
|
/>
|
|
<div class="grid min-w-0 gap-1">
|
|
<Label for="is_blocked">
|
|
{{ t('ticketDetail.blocked') }}
|
|
</Label>
|
|
<p
|
|
id="ticket-blocker-state-description"
|
|
class="text-muted-foreground text-sm"
|
|
>
|
|
{{
|
|
isBlocked
|
|
? t(
|
|
'ticketDetail.blockerActiveDescription',
|
|
)
|
|
: t(
|
|
'ticketDetail.blockerInactiveDescription',
|
|
)
|
|
}}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<InputError
|
|
id="is_blocked-error"
|
|
:message="form.errors.is_blocked"
|
|
/>
|
|
|
|
<TicketFormField
|
|
v-if="isBlocked"
|
|
id="blocked_comment"
|
|
v-model="form.blocked_comment"
|
|
kind="textarea"
|
|
:rows="3"
|
|
:label="t('ticketDetail.blockerComment')"
|
|
:hint="t('ticketDetail.blockerCommentHelp')"
|
|
:placeholder="t('ticketDetail.blockerPlaceholder')"
|
|
:error="form.errors.blocked_comment"
|
|
:disabled="form.processing"
|
|
required
|
|
/>
|
|
|
|
<div
|
|
class="flex flex-col-reverse gap-3 sm:flex-row sm:justify-end"
|
|
>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
:disabled="!form.isDirty || form.processing"
|
|
@click="reset"
|
|
>
|
|
{{ t('ticketDetail.resetBlocker') }}
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
:loading="form.processing"
|
|
:loading-label="t('ticketDetail.savingBlocker')"
|
|
:disabled="!form.isDirty"
|
|
>
|
|
{{
|
|
isBlocked
|
|
? t('ticketDetail.saveBlocker')
|
|
: t('ticketDetail.unblockTicket')
|
|
}}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
|
|
<CardContent v-else>
|
|
<p
|
|
class="whitespace-pre-wrap text-sm leading-6"
|
|
:class="{ 'text-muted-foreground': !isBlocked }"
|
|
>
|
|
{{
|
|
isBlocked
|
|
? ticket.blocked_comment
|
|
: t('ticketDetail.blockerInactiveDescription')
|
|
}}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
</template>
|