Add checklist acceptance criteria

This commit is contained in:
2026-04-24 20:48:59 +00:00
parent 68bc5d8915
commit af6fc66e09
16 changed files with 961 additions and 51 deletions
+132 -19
View File
@@ -1,11 +1,12 @@
<script setup lang="ts">
import { Head, Link, useForm } from '@inertiajs/vue3';
import { computed } from 'vue';
import { computed, ref } from 'vue';
import TicketPriorityIcon from '@/components/TicketPriorityIcon.vue';
import TicketTypeIcon from '@/components/TicketTypeIcon.vue';
import InputError from '@/components/InputError.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Card,
CardContent,
@@ -24,6 +25,10 @@ import {
} from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import AppLayout from '@/layouts/AppLayout.vue';
import {
buildAcceptanceCriteria,
parseChecklistLines,
} from '@/lib/acceptance-criteria';
import { index as backlogIndex } from '@/routes/projects/backlog';
import { index as boardIndex } from '@/routes/projects/board';
import { index, show } from '@/routes/projects/index';
@@ -79,8 +84,14 @@ const hasTicketMetadata =
props.ticketPriorities.length > 0 &&
props.ticketStatusOptions.length > 0;
const acceptanceCriteriaSummary = ref('');
const acceptanceCriteriaChecklist = ref('');
const form = useForm({
acceptance_criteria: '',
acceptance_criteria: buildAcceptanceCriteria(
acceptanceCriteriaSummary.value,
acceptanceCriteriaChecklist.value,
),
assignee_user_id: '',
description: '',
story_points: '',
@@ -92,6 +103,22 @@ const form = useForm({
title: '',
});
const acceptanceCriteriaPreviewItems = computed(() =>
parseChecklistLines(acceptanceCriteriaChecklist.value),
);
const acceptanceCriteriaError = computed(() => {
return (
form.errors.acceptance_criteria ??
form.errors['acceptance_criteria.summary'] ??
form.errors['acceptance_criteria.items'] ??
Object.entries(form.errors).find(([key]) =>
key.startsWith('acceptance_criteria.items.'),
)?.[1] ??
''
);
});
const selectedTicketType = computed(
() =>
props.ticketTypes.find(
@@ -121,6 +148,11 @@ const selectedAssignee = computed(
);
const submit = (): void => {
form.acceptance_criteria = buildAcceptanceCriteria(
acceptanceCriteriaSummary.value,
acceptanceCriteriaChecklist.value,
);
form.post(`/projects/${props.project.id}/tickets`, {
preserveScroll: true,
});
@@ -298,25 +330,106 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
/>
</div>
<div class="grid gap-2">
<Label
for="acceptance_criteria"
:class="fieldLabelClass"
>
Acceptance criteria
</Label>
<Textarea
id="acceptance_criteria"
v-model="form.acceptance_criteria"
:disabled="form.processing"
placeholder="List the conditions that must be true before this ticket is done."
:class="textareaClass"
/>
<div class="grid gap-4">
<div class="grid gap-2">
<Label
for="acceptance_criteria_summary"
:class="fieldLabelClass"
>
Acceptance criteria summary
</Label>
<Input
id="acceptance_criteria_summary"
v-model="acceptanceCriteriaSummary"
:disabled="form.processing"
placeholder="In one sentence, what defines done?"
:class="inputClass"
/>
</div>
<div class="grid gap-2">
<Label
for="acceptance_criteria_checklist"
:class="fieldLabelClass"
>
Acceptance checklist
</Label>
<Textarea
id="acceptance_criteria_checklist"
v-model="acceptanceCriteriaChecklist"
:disabled="form.processing"
placeholder="[ ] User can sign in\n[ ] Errors are shown inline\n [x] Nested checks use two-space indent"
:class="textareaClass"
/>
<p
class="text-muted-foreground text-xs leading-5"
>
Each line is a checklist item.
Prefix with
<span class="font-semibold"
>[x]</span
>
for checked items and indent with
two spaces for nested items.
</p>
</div>
<InputError
:message="
form.errors.acceptance_criteria
"
:message="acceptanceCriteriaError"
/>
<div
class="dark:bg-muted/25 grid gap-3 rounded-[1.25rem] border border-dashed border-[#adb3b5]/35 bg-[#f8f9fa]/70 p-4 dark:border-white/10"
>
<p
class="text-[0.72rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
>
Checklist preview
</p>
<div
v-if="
acceptanceCriteriaPreviewItems.length ===
0
"
class="text-muted-foreground rounded-xl border border-dashed px-3 py-3 text-sm"
>
Add checklist lines to preview
nested items.
</div>
<div v-else class="space-y-2">
<div
v-for="(
checklistItem,
checklistItemIndex
) in acceptanceCriteriaPreviewItems"
:key="checklistItemIndex"
class="flex items-start gap-3"
:style="{
marginLeft: `${checklistItem.indent * 1.25}rem`,
}"
>
<Checkbox
:checked="
checklistItem.checked
"
disabled
class="mt-0.5"
/>
<span
class="text-sm"
:class="
checklistItem.checked
? 'text-muted-foreground line-through'
: 'text-foreground'
"
>
{{ checklistItem.text }}
</span>
</div>
</div>
</div>
</div>
</div>