809 lines
41 KiB
Vue
809 lines
41 KiB
Vue
<script setup lang="ts">
|
|
import { Head, Link, useForm } from '@inertiajs/vue3';
|
|
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,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} 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';
|
|
import { metadata as metadataSettings } from '@/routes/projects/settings';
|
|
import type { BreadcrumbItem, ProjectTicketCreatePageProps } from '@/types';
|
|
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
|
|
|
const props = defineProps<ProjectTicketCreatePageProps>();
|
|
|
|
const frostedCardClass =
|
|
'gap-0 overflow-hidden rounded-[1.75rem] border-white/70 bg-white/85 shadow-[0_22px_65px_-40px_rgba(45,51,53,0.38)] backdrop-blur-sm dark:border-white/10 dark:bg-card/90 dark:shadow-black/25';
|
|
const airyOutlineButtonClass =
|
|
'rounded-full border-white/70 bg-white/80 text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.34)] hover:bg-white dark:border-white/10 dark:bg-card/85 dark:text-foreground dark:hover:bg-card';
|
|
const inputClass =
|
|
'h-12 rounded-xl border-white/70 bg-white/80 text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.22)] focus-visible:border-[#9de77c] focus-visible:ring-[#abf688]/35 dark:border-white/10 dark:bg-card/90 dark:text-foreground dark:focus-visible:border-primary';
|
|
const textareaClass =
|
|
'min-h-32 rounded-[1.25rem] border-white/70 bg-white/80 text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.22)] focus-visible:border-[#9de77c] focus-visible:ring-[#abf688]/35 dark:border-white/10 dark:bg-card/90 dark:text-foreground dark:focus-visible:border-primary';
|
|
const selectTriggerClass =
|
|
'h-12 w-full rounded-xl border-white/70 bg-white/80 text-left text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.22)] focus:border-[#9de77c] focus:ring-[#abf688]/35 dark:border-white/10 dark:bg-card/90 dark:text-foreground dark:focus:border-primary';
|
|
const selectContentClass =
|
|
'rounded-[1.25rem] border-white/80 bg-white/95 shadow-[0_24px_60px_-40px_rgba(45,51,53,0.34)] backdrop-blur-sm dark:border-white/10 dark:bg-card/95';
|
|
const fieldLabelClass =
|
|
'text-[0.72rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]';
|
|
|
|
const returnHref =
|
|
props.source === 'board'
|
|
? boardIndex(props.project.id)
|
|
: backlogIndex(props.project.id);
|
|
const returnLabel =
|
|
props.source === 'board' ? 'Back to board' : 'Back to backlog';
|
|
|
|
const breadcrumbs: BreadcrumbItem[] = [
|
|
{
|
|
title: 'Projects',
|
|
href: index(),
|
|
},
|
|
{
|
|
title: props.project.name,
|
|
href: show(props.project.id),
|
|
},
|
|
{
|
|
title: props.source === 'board' ? 'Board' : 'Backlog',
|
|
href: returnHref,
|
|
},
|
|
{
|
|
title: 'Create ticket',
|
|
href: `/projects/${props.project.id}/tickets/create?source=${props.source}`,
|
|
},
|
|
];
|
|
|
|
const hasTicketMetadata =
|
|
props.ticketTypes.length > 0 &&
|
|
props.ticketPriorities.length > 0 &&
|
|
props.ticketStatusOptions.length > 0;
|
|
|
|
const acceptanceCriteriaSummary = ref('');
|
|
const acceptanceCriteriaChecklist = ref('');
|
|
|
|
const form = useForm({
|
|
acceptance_criteria: buildAcceptanceCriteria(
|
|
acceptanceCriteriaSummary.value,
|
|
acceptanceCriteriaChecklist.value,
|
|
),
|
|
assignee_user_id: '',
|
|
description: '',
|
|
story_points: '',
|
|
ticket_priority_id: String(props.ticketPriorities[0]?.id ?? ''),
|
|
ticket_status_id: String(
|
|
props.defaultTicketStatusId ?? props.ticketStatusOptions[0]?.id ?? '',
|
|
),
|
|
ticket_type_id: String(props.ticketTypes[0]?.id ?? ''),
|
|
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(
|
|
(ticketType) => String(ticketType.id) === form.ticket_type_id,
|
|
) ?? null,
|
|
);
|
|
|
|
const selectedTicketPriority = computed(
|
|
() =>
|
|
props.ticketPriorities.find(
|
|
(ticketPriority) =>
|
|
String(ticketPriority.id) === form.ticket_priority_id,
|
|
) ?? null,
|
|
);
|
|
|
|
const selectedTicketStatus = computed(
|
|
() =>
|
|
props.ticketStatusOptions.find(
|
|
(ticketStatus) => String(ticketStatus.id) === form.ticket_status_id,
|
|
) ?? null,
|
|
);
|
|
|
|
const selectedAssignee = computed(
|
|
() =>
|
|
props.users.find((user) => String(user.id) === form.assignee_user_id) ??
|
|
null,
|
|
);
|
|
|
|
const submit = (): void => {
|
|
form.acceptance_criteria = buildAcceptanceCriteria(
|
|
acceptanceCriteriaSummary.value,
|
|
acceptanceCriteriaChecklist.value,
|
|
);
|
|
|
|
form.post(`/projects/${props.project.id}/tickets`, {
|
|
preserveScroll: true,
|
|
});
|
|
};
|
|
|
|
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}`;
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<AppLayout :breadcrumbs="breadcrumbs">
|
|
<Head :title="`Create ${project.name} ticket`" />
|
|
|
|
<div class="relative isolate flex flex-1 flex-col gap-8 px-4 py-6">
|
|
<div
|
|
class="pointer-events-none absolute inset-x-0 top-0 -z-10 h-72 bg-[radial-gradient(circle_at_top_left,rgba(171,246,136,0.34),transparent_42%),radial-gradient(circle_at_top_right,rgba(216,231,203,0.7),transparent_34%)]"
|
|
/>
|
|
|
|
<section
|
|
class="rounded-[2rem] border border-white/70 bg-[linear-gradient(135deg,rgba(255,255,255,0.94)_0%,rgba(241,244,245,0.9)_100%)] p-6 shadow-[0_28px_80px_-48px_rgba(45,51,53,0.4)] backdrop-blur-sm dark:border-white/10 dark:bg-[linear-gradient(135deg,rgba(17,20,20,0.92)_0%,rgba(24,28,28,0.88)_100%)] dark:shadow-black/30"
|
|
>
|
|
<div
|
|
class="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between"
|
|
>
|
|
<div class="max-w-3xl space-y-4">
|
|
<div class="flex flex-wrap items-center gap-2.5">
|
|
<Badge
|
|
variant="outline"
|
|
class="dark:bg-card/85 dark:text-muted-foreground rounded-full border-white/70 bg-white/80 px-3 py-1 text-[0.68rem] font-semibold uppercase tracking-[0.2em] text-[#5a6062] shadow-[0_16px_40px_-34px_rgba(45,51,53,0.3)] dark:border-white/10"
|
|
>
|
|
{{ project.key }}
|
|
</Badge>
|
|
|
|
<Badge
|
|
variant="outline"
|
|
class="dark:bg-card/80 dark:text-foreground gap-2 rounded-full border-white/70 bg-white/70 px-3 py-1.5 text-[#2d3335] shadow-[0_16px_40px_-34px_rgba(45,51,53,0.3)] dark:border-white/10"
|
|
>
|
|
<TicketTypeIcon class="size-5" />
|
|
{{
|
|
selectedTicketType?.name ?? 'Choose a type'
|
|
}}
|
|
</Badge>
|
|
|
|
<Badge
|
|
variant="outline"
|
|
class="dark:bg-card/80 dark:text-foreground gap-2 rounded-full border-white/70 bg-white/70 px-3 py-1.5 text-[#2d3335] shadow-[0_16px_40px_-34px_rgba(45,51,53,0.3)] dark:border-white/10"
|
|
>
|
|
<TicketPriorityIcon class="size-5" />
|
|
{{
|
|
selectedTicketPriority?.name ??
|
|
'Choose a priority'
|
|
}}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div class="space-y-3">
|
|
<p
|
|
class="dark:text-muted-foreground text-[0.72rem] font-semibold uppercase tracking-[0.22em] text-[#5a6062]"
|
|
>
|
|
Ticket creation
|
|
</p>
|
|
<h1
|
|
class="font-display dark:text-foreground text-3xl font-semibold tracking-tight text-[#2d3335] md:text-4xl"
|
|
>
|
|
Create a ticket with the new Stitch workspace
|
|
styling.
|
|
</h1>
|
|
<p
|
|
class="dark:text-muted-foreground max-w-2xl text-sm leading-7 text-[#5a6062]"
|
|
>
|
|
Build out scope, intent, and acceptance criteria
|
|
in one pass. The ticket key is created
|
|
automatically once the draft is saved.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-3">
|
|
<Button
|
|
as-child
|
|
variant="outline"
|
|
:class="airyOutlineButtonClass"
|
|
>
|
|
<Link :href="returnHref">{{ returnLabel }}</Link>
|
|
</Button>
|
|
|
|
<Button
|
|
as-child
|
|
variant="outline"
|
|
:class="airyOutlineButtonClass"
|
|
>
|
|
<Link :href="show(project.id)"
|
|
>Back to project</Link
|
|
>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div
|
|
v-if="status"
|
|
class="rounded-[1.5rem] border border-emerald-200/80 bg-emerald-50/80 px-5 py-4 text-sm text-emerald-900 shadow-[0_20px_45px_-38px_rgba(16,185,129,0.45)] dark:border-emerald-900/60 dark:bg-emerald-950/30 dark:text-emerald-100"
|
|
>
|
|
{{ status }}
|
|
</div>
|
|
|
|
<div
|
|
v-if="hasTicketMetadata"
|
|
class="grid gap-6 xl:grid-cols-[minmax(0,1.45fr)_22rem]"
|
|
>
|
|
<Card :class="frostedCardClass">
|
|
<CardHeader class="border-b border-[#adb3b5]/20 pb-6">
|
|
<CardTitle
|
|
class="font-display dark:text-foreground text-2xl text-[#2d3335]"
|
|
>
|
|
Ticket blueprint
|
|
</CardTitle>
|
|
<CardDescription class="max-w-2xl leading-6">
|
|
Capture the essence of the work first, then define
|
|
the delivery frame with type, priority, status, and
|
|
ownership.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent class="p-6">
|
|
<form class="grid gap-6" @submit.prevent="submit">
|
|
<div class="grid gap-2">
|
|
<Label for="title" :class="fieldLabelClass">
|
|
Title
|
|
</Label>
|
|
<Input
|
|
id="title"
|
|
v-model="form.title"
|
|
required
|
|
autofocus
|
|
:disabled="form.processing"
|
|
placeholder="Implement repository sync"
|
|
:class="inputClass"
|
|
/>
|
|
<InputError :message="form.errors.title" />
|
|
</div>
|
|
|
|
<div
|
|
class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]"
|
|
>
|
|
<div class="grid gap-6">
|
|
<div class="grid gap-2">
|
|
<Label
|
|
for="description"
|
|
:class="fieldLabelClass"
|
|
>
|
|
Description
|
|
</Label>
|
|
<Textarea
|
|
id="description"
|
|
v-model="form.description"
|
|
:disabled="form.processing"
|
|
placeholder="Describe the expected behavior, context, and why this work matters."
|
|
:class="textareaClass"
|
|
/>
|
|
<InputError
|
|
:message="form.errors.description"
|
|
/>
|
|
</div>
|
|
|
|
<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="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>
|
|
|
|
<div
|
|
class="dark:bg-muted/40 grid h-fit gap-5 rounded-[1.5rem] bg-[#f1f4f5]/90 p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.75)]"
|
|
>
|
|
<div class="grid gap-2">
|
|
<Label
|
|
for="ticket_type_id"
|
|
:class="fieldLabelClass"
|
|
>
|
|
Type
|
|
</Label>
|
|
<Select
|
|
v-model="form.ticket_type_id"
|
|
:disabled="form.processing"
|
|
>
|
|
<SelectTrigger
|
|
id="ticket_type_id"
|
|
:class="selectTriggerClass"
|
|
>
|
|
<SelectValue
|
|
placeholder="Choose a type"
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent
|
|
:class="selectContentClass"
|
|
>
|
|
<SelectItem
|
|
v-for="ticketType in ticketTypes"
|
|
:key="ticketType.id"
|
|
:value="
|
|
String(ticketType.id)
|
|
"
|
|
>
|
|
<TicketTypeIcon
|
|
class="size-5"
|
|
/>
|
|
<span>
|
|
{{ ticketType.name }}
|
|
</span>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<InputError
|
|
:message="
|
|
form.errors.ticket_type_id
|
|
"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label
|
|
for="ticket_priority_id"
|
|
:class="fieldLabelClass"
|
|
>
|
|
Priority
|
|
</Label>
|
|
<Select
|
|
v-model="form.ticket_priority_id"
|
|
:disabled="form.processing"
|
|
>
|
|
<SelectTrigger
|
|
id="ticket_priority_id"
|
|
:class="selectTriggerClass"
|
|
>
|
|
<SelectValue
|
|
placeholder="Choose a priority"
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent
|
|
:class="selectContentClass"
|
|
>
|
|
<SelectItem
|
|
v-for="ticketPriority in ticketPriorities"
|
|
:key="ticketPriority.id"
|
|
:value="
|
|
String(
|
|
ticketPriority.id,
|
|
)
|
|
"
|
|
>
|
|
<TicketPriorityIcon
|
|
class="size-5"
|
|
/>
|
|
<span>
|
|
{{
|
|
ticketPriority.name
|
|
}}
|
|
</span>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<InputError
|
|
:message="
|
|
form.errors.ticket_priority_id
|
|
"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label
|
|
for="ticket_status_id"
|
|
:class="fieldLabelClass"
|
|
>
|
|
Status
|
|
</Label>
|
|
<Select
|
|
v-model="form.ticket_status_id"
|
|
:disabled="form.processing"
|
|
>
|
|
<SelectTrigger
|
|
id="ticket_status_id"
|
|
:class="selectTriggerClass"
|
|
>
|
|
<SelectValue
|
|
placeholder="Choose a status"
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent
|
|
:class="selectContentClass"
|
|
>
|
|
<SelectItem
|
|
v-for="ticketStatus in ticketStatusOptions"
|
|
:key="ticketStatus.id"
|
|
:value="
|
|
String(ticketStatus.id)
|
|
"
|
|
>
|
|
{{
|
|
ticketStatusLabel(
|
|
ticketStatus,
|
|
)
|
|
}}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<InputError
|
|
:message="
|
|
form.errors.ticket_status_id
|
|
"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid gap-2">
|
|
<Label
|
|
for="assignee_user_id"
|
|
:class="fieldLabelClass"
|
|
>
|
|
Assignee
|
|
</Label>
|
|
<Select
|
|
v-model="form.assignee_user_id"
|
|
:disabled="form.processing"
|
|
>
|
|
<SelectTrigger
|
|
id="assignee_user_id"
|
|
:class="selectTriggerClass"
|
|
>
|
|
<SelectValue
|
|
placeholder="Unassigned"
|
|
/>
|
|
</SelectTrigger>
|
|
<SelectContent
|
|
:class="selectContentClass"
|
|
>
|
|
<SelectItem value="">
|
|
Unassigned
|
|
</SelectItem>
|
|
<SelectItem
|
|
v-for="user in users"
|
|
:key="user.id"
|
|
:value="String(user.id)"
|
|
>
|
|
{{ user.name }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<InputError
|
|
:message="
|
|
form.errors.assignee_user_id
|
|
"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="project.show_story_points"
|
|
class="grid gap-2"
|
|
>
|
|
<Label
|
|
for="story_points"
|
|
:class="fieldLabelClass"
|
|
>
|
|
Story points
|
|
</Label>
|
|
<Input
|
|
id="story_points"
|
|
v-model="form.story_points"
|
|
type="number"
|
|
step="1"
|
|
:disabled="form.processing"
|
|
:class="inputClass"
|
|
/>
|
|
<InputError
|
|
:message="form.errors.story_points"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-3 pt-2">
|
|
<Button
|
|
type="submit"
|
|
:disabled="form.processing"
|
|
class="rounded-full bg-[linear-gradient(135deg,#1e6000_0%,#2a6d0e_55%,#6ab04c_100%)] px-6 text-[#ecffdd] shadow-[0_20px_45px_-28px_rgba(42,109,14,0.7)] hover:opacity-95"
|
|
>
|
|
Create ticket
|
|
</Button>
|
|
<Button
|
|
as-child
|
|
variant="outline"
|
|
:class="airyOutlineButtonClass"
|
|
>
|
|
<Link :href="returnHref">
|
|
{{ returnLabel }}
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div class="space-y-6 xl:sticky xl:top-24 xl:self-start">
|
|
<Card :class="frostedCardClass">
|
|
<CardHeader class="border-b border-[#adb3b5]/20 pb-5">
|
|
<CardTitle
|
|
class="font-display dark:text-foreground text-xl text-[#2d3335]"
|
|
>
|
|
Draft snapshot
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Keep the setup visible while you shape the
|
|
ticket.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
|
|
<CardContent class="space-y-4 p-6">
|
|
<div
|
|
class="dark:bg-muted/40 rounded-[1.5rem] bg-[#f1f4f5] p-4 shadow-[inset_0_1px_0_rgba(255,255,255,0.85)]"
|
|
>
|
|
<p
|
|
class="dark:text-muted-foreground text-[0.72rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
|
>
|
|
Ticket key
|
|
</p>
|
|
<p
|
|
class="font-display dark:text-foreground mt-2 text-2xl font-semibold text-[#2d3335]"
|
|
>
|
|
Assigned after creation
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid gap-3">
|
|
<div
|
|
class="dark:bg-muted/25 flex items-center justify-between rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
|
>
|
|
<span
|
|
class="dark:text-muted-foreground text-sm text-[#5a6062]"
|
|
>
|
|
Type
|
|
</span>
|
|
<span
|
|
class="dark:text-foreground flex items-center gap-2 text-sm font-medium text-[#2d3335]"
|
|
>
|
|
<TicketTypeIcon class="size-5" />
|
|
{{
|
|
selectedTicketType?.name ??
|
|
'Choose a type'
|
|
}}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
class="dark:bg-muted/25 flex items-center justify-between rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
|
>
|
|
<span
|
|
class="dark:text-muted-foreground text-sm text-[#5a6062]"
|
|
>
|
|
Priority
|
|
</span>
|
|
<span
|
|
class="dark:text-foreground flex items-center gap-2 text-sm font-medium text-[#2d3335]"
|
|
>
|
|
<TicketPriorityIcon class="size-5" />
|
|
{{
|
|
selectedTicketPriority?.name ??
|
|
'Choose a priority'
|
|
}}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
class="dark:bg-muted/25 flex items-center justify-between rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
|
>
|
|
<span
|
|
class="dark:text-muted-foreground text-sm text-[#5a6062]"
|
|
>
|
|
Status
|
|
</span>
|
|
<span
|
|
class="dark:text-foreground text-sm font-medium text-[#2d3335]"
|
|
>
|
|
{{
|
|
selectedTicketStatus
|
|
? ticketStatusLabel(
|
|
selectedTicketStatus,
|
|
)
|
|
: 'Choose a status'
|
|
}}
|
|
</span>
|
|
</div>
|
|
|
|
<div
|
|
class="dark:bg-muted/25 flex items-center justify-between rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
|
>
|
|
<span
|
|
class="dark:text-muted-foreground text-sm text-[#5a6062]"
|
|
>
|
|
Assignee
|
|
</span>
|
|
<span
|
|
class="dark:text-foreground text-sm font-medium text-[#2d3335]"
|
|
>
|
|
{{
|
|
selectedAssignee?.name ??
|
|
'Unassigned'
|
|
}}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="dark:bg-card/60 dark:text-muted-foreground rounded-[1.5rem] border border-dashed border-[#adb3b5]/45 bg-white/55 px-4 py-4 text-sm leading-6 text-[#5a6062] dark:border-white/10"
|
|
>
|
|
Ticket types, priorities, and statuses come
|
|
directly from the current project metadata.
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<Card v-else :class="frostedCardClass">
|
|
<CardContent class="flex flex-col gap-4 p-6 text-sm">
|
|
<p
|
|
class="dark:text-muted-foreground leading-7 text-[#5a6062]"
|
|
>
|
|
Ticket creation needs at least one type, one priority,
|
|
and one status.
|
|
</p>
|
|
<div>
|
|
<Button
|
|
as-child
|
|
variant="outline"
|
|
:class="airyOutlineButtonClass"
|
|
>
|
|
<Link :href="metadataSettings(project.id)">
|
|
Open ticket settings
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</AppLayout>
|
|
</template>
|