Adopt Stitch styling for ticket pages
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue';
|
||||||
|
import { MessageSquareDashed } from 'lucide-vue-next';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
class?: HTMLAttributes['class'];
|
||||||
|
description?: string;
|
||||||
|
feature: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
'This area is already part of the Stitch concept and will be wired into the app in a later pass.',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'rounded-[1.75rem] border border-[#e7de92] bg-[linear-gradient(180deg,#fffbe3_0%,#fdf6b0_100%)] p-5 text-[#615d27] shadow-[0_24px_60px_-42px_rgba(97,93,39,0.45)] dark:border-[#6c6830]/70 dark:bg-[linear-gradient(180deg,rgba(101,97,42,0.24)_0%,rgba(79,75,22,0.42)_100%)] dark:text-[#fdf6b0]',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center rounded-full border border-[#ddd382] bg-white/65 px-3 py-1 text-[0.68rem] font-semibold uppercase tracking-[0.2em] text-[#6c6830] dark:border-[#6c6830] dark:bg-black/15 dark:text-[#fdf6b0]"
|
||||||
|
>
|
||||||
|
Coming soon
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="flex size-11 shrink-0 items-center justify-center rounded-2xl bg-white/70 text-[#65612a] shadow-[inset_0_1px_0_rgba(255,255,255,0.7)] dark:bg-black/20 dark:text-[#fdf6b0]"
|
||||||
|
>
|
||||||
|
<MessageSquareDashed class="size-5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="font-display text-lg font-semibold">
|
||||||
|
{{ feature }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm leading-6 text-[#7d7737] dark:text-[#eee8a3]">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue';
|
||||||
|
import { Flag } from 'lucide-vue-next';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class'];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'dark:bg-primary/20 dark:text-primary inline-flex size-6 items-center justify-center rounded-full bg-[#d8e7cb] text-[#1d5e00] shadow-[inset_0_1px_0_rgba(255,255,255,0.6)]',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Flag class="size-3.5" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue';
|
||||||
|
import { Shapes } from 'lucide-vue-next';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class'];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'dark:bg-secondary/40 dark:text-secondary-foreground inline-flex size-6 items-center justify-center rounded-full bg-[#dee3e6] text-[#485640] shadow-[inset_0_1px_0_rgba(255,255,255,0.6)]',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Shapes class="size-3.5" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
@@ -5,9 +5,11 @@ import { computed, ref, watch } from 'vue';
|
|||||||
import ProjectTicketBranchController from '@/actions/App/Http/Controllers/ProjectTicketBranchController';
|
import ProjectTicketBranchController from '@/actions/App/Http/Controllers/ProjectTicketBranchController';
|
||||||
import ProjectTicketLinkController from '@/actions/App/Http/Controllers/ProjectTicketLinkController';
|
import ProjectTicketLinkController from '@/actions/App/Http/Controllers/ProjectTicketLinkController';
|
||||||
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
|
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
|
||||||
import Heading from '@/components/Heading.vue';
|
import FeatureComingSoonPanel from '@/components/FeatureComingSoonPanel.vue';
|
||||||
import InputError from '@/components/InputError.vue';
|
import InputError from '@/components/InputError.vue';
|
||||||
|
import TicketPriorityIcon from '@/components/TicketPriorityIcon.vue';
|
||||||
import TicketTreeList from '@/components/TicketTreeList.vue';
|
import TicketTreeList from '@/components/TicketTreeList.vue';
|
||||||
|
import TicketTypeIcon from '@/components/TicketTypeIcon.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@@ -25,8 +27,17 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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 AppLayout from '@/layouts/AppLayout.vue';
|
||||||
import { edit as editGitConnections } from '@/routes/git-connections/index';
|
import { edit as editGitConnections } from '@/routes/git-connections/index';
|
||||||
import { index as backlogIndex } from '@/routes/projects/backlog';
|
import { index as backlogIndex } from '@/routes/projects/backlog';
|
||||||
@@ -103,6 +114,25 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const frostedCardClass =
|
||||||
|
'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 frostedAsideCardClass =
|
||||||
|
'h-fit 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 nativeSelectClass =
|
||||||
|
'h-12 w-full rounded-xl border border-white/70 bg-white/80 px-3 py-2 text-sm text-[#2d3335] shadow-[0_18px_45px_-34px_rgba(45,51,53,0.22)] outline-none transition-[color,box-shadow] focus:border-[#9de77c] focus:ring-4 focus:ring-[#abf688]/35 disabled:cursor-not-allowed disabled:opacity-60 dark:border-white/10 dark:bg-card/90 dark:text-foreground dark:focus:border-primary dark:focus:ring-primary/20';
|
||||||
|
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 form = useForm({
|
const form = useForm({
|
||||||
acceptance_criteria: props.ticket.acceptance_criteria ?? '',
|
acceptance_criteria: props.ticket.acceptance_criteria ?? '',
|
||||||
assignee_user_id: props.ticket.assignee_user_id ?? '',
|
assignee_user_id: props.ticket.assignee_user_id ?? '',
|
||||||
@@ -124,12 +154,56 @@ const form = useForm({
|
|||||||
}),
|
}),
|
||||||
is_blocked: props.ticket.is_blocked,
|
is_blocked: props.ticket.is_blocked,
|
||||||
parent_ticket_id: props.ticket.parent_ticket_id ?? '',
|
parent_ticket_id: props.ticket.parent_ticket_id ?? '',
|
||||||
ticket_priority_id: props.ticket.ticket_priority_id,
|
ticket_priority_id: String(props.ticket.ticket_priority_id),
|
||||||
ticket_type_id: props.ticket.ticket_type_id,
|
ticket_type_id: String(props.ticket.ticket_type_id),
|
||||||
title: props.ticket.title,
|
title: props.ticket.title,
|
||||||
story_points: props.ticket.story_points,
|
story_points: props.ticket.story_points,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedTicketType = computed(
|
||||||
|
() =>
|
||||||
|
props.ticketTypes.find(
|
||||||
|
(ticketType) => String(ticketType.id) === form.ticket_type_id,
|
||||||
|
) ?? props.ticket.ticket_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedTicketPriority = computed(
|
||||||
|
() =>
|
||||||
|
props.ticketPriorities.find(
|
||||||
|
(ticketPriority) =>
|
||||||
|
String(ticketPriority.id) === form.ticket_priority_id,
|
||||||
|
) ?? props.ticket.ticket_priority,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedAssignee = computed(
|
||||||
|
() =>
|
||||||
|
props.users.find(
|
||||||
|
(user) => String(user.id) === String(form.assignee_user_id),
|
||||||
|
) ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedAuthor = computed(
|
||||||
|
() =>
|
||||||
|
props.users.find(
|
||||||
|
(user) => String(user.id) === String(form.author_user_id),
|
||||||
|
) ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const repositoryBranchCount = computed(() =>
|
||||||
|
visibleRepositoryBranches.value.reduce(
|
||||||
|
(count, repositoryBranch) => count + repositoryBranch.branches.length,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pullRequestCount = computed(() =>
|
||||||
|
visibleRepositoryPullRequests.value.reduce(
|
||||||
|
(count, repositoryPullRequest) =>
|
||||||
|
count + repositoryPullRequest.pull_requests.length,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const isCreateBranchDialogOpen = ref(false);
|
const isCreateBranchDialogOpen = ref(false);
|
||||||
const isCheckingBranchName = ref(false);
|
const isCheckingBranchName = ref(false);
|
||||||
const branchNameValidation = ref<BranchNameValidationModel | null>(null);
|
const branchNameValidation = ref<BranchNameValidationModel | null>(null);
|
||||||
@@ -271,8 +345,7 @@ watch(
|
|||||||
|
|
||||||
branchNameValidation.value = {
|
branchNameValidation.value = {
|
||||||
is_valid: false,
|
is_valid: false,
|
||||||
message:
|
message: `${appName} could not validate this branch name right now.`,
|
||||||
`${appName} could not validate this branch name right now.`,
|
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
if (!abortController.signal.aborted) {
|
if (!abortController.signal.aborted) {
|
||||||
@@ -341,65 +414,139 @@ const formatDateTime = (value: string): string => {
|
|||||||
<AppLayout :breadcrumbs="breadcrumbs">
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||||||
<Head :title="ticket.ticket_key" />
|
<Head :title="ticket.ticket_key" />
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-6 p-4">
|
<div class="relative isolate flex flex-1 flex-col gap-8 px-4 py-6">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
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="space-y-3">
|
<div
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
class="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between"
|
||||||
<Badge variant="outline" class="px-3 py-1">
|
>
|
||||||
{{ ticket.ticket_key }}
|
<div class="max-w-3xl space-y-4">
|
||||||
</Badge>
|
<div class="flex flex-wrap items-center gap-2.5">
|
||||||
<Badge variant="secondary">
|
<Badge
|
||||||
{{ ticket.ticket_type.name }}
|
variant="outline"
|
||||||
</Badge>
|
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"
|
||||||
<Badge variant="secondary">
|
>
|
||||||
{{ ticket.ticket_priority.name }}
|
{{ ticket.ticket_key }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge v-if="ticket.ticket_status" variant="outline">
|
|
||||||
{{ ticket.ticket_status.name }}
|
<Badge
|
||||||
</Badge>
|
variant="outline"
|
||||||
<Badge v-if="!canEditTicket" 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"
|
||||||
Read only
|
>
|
||||||
</Badge>
|
<TicketTypeIcon class="size-5" />
|
||||||
|
{{ selectedTicketType.name }}
|
||||||
|
</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 }}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
v-if="ticket.ticket_status"
|
||||||
|
variant="outline"
|
||||||
|
class="dark:bg-card/80 dark:text-foreground 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"
|
||||||
|
>
|
||||||
|
{{ ticket.ticket_status.name }}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
v-if="ticket.is_blocked"
|
||||||
|
class="rounded-full border-transparent bg-[#fdf6b0] px-3 py-1.5 text-[#615d27] dark:bg-[#65612a]/35 dark:text-[#fdf6b0]"
|
||||||
|
>
|
||||||
|
Blocked
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
v-if="!canEditTicket"
|
||||||
|
variant="outline"
|
||||||
|
class="dark:bg-card/80 dark:text-muted-foreground rounded-full border-white/70 bg-white/70 px-3 py-1.5 text-[#5a6062] shadow-[0_16px_40px_-34px_rgba(45,51,53,0.3)] dark:border-white/10"
|
||||||
|
>
|
||||||
|
Read only
|
||||||
|
</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 detail
|
||||||
|
</p>
|
||||||
|
<h1
|
||||||
|
class="font-display dark:text-foreground text-3xl font-semibold tracking-tight text-[#2d3335] md:text-4xl"
|
||||||
|
>
|
||||||
|
{{ ticket.title }}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground max-w-2xl text-sm leading-7 text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Review ticket details, update tracked fields,
|
||||||
|
and inspect the change timeline in the new
|
||||||
|
Stitch-inspired workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Heading
|
|
||||||
:title="ticket.title"
|
|
||||||
description="Review ticket details, update tracked fields, and inspect the change timeline."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<Button as-child variant="outline">
|
<Button
|
||||||
<Link :href="backlogIndex(project.id)"
|
as-child
|
||||||
>Open backlog</Link
|
variant="outline"
|
||||||
|
:class="airyOutlineButtonClass"
|
||||||
>
|
>
|
||||||
</Button>
|
<Link :href="backlogIndex(project.id)">
|
||||||
|
Open backlog
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button as-child variant="outline">
|
<Button
|
||||||
<Link :href="boardIndex(project.id)">Open board</Link>
|
as-child
|
||||||
</Button>
|
variant="outline"
|
||||||
|
:class="airyOutlineButtonClass"
|
||||||
|
>
|
||||||
|
<Link :href="boardIndex(project.id)"
|
||||||
|
>Open board</Link
|
||||||
|
>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button as-child variant="outline">
|
<Button
|
||||||
<Link :href="show(project.id)">Back to project</Link>
|
as-child
|
||||||
</Button>
|
variant="outline"
|
||||||
|
:class="airyOutlineButtonClass"
|
||||||
|
>
|
||||||
|
<Link :href="show(project.id)"
|
||||||
|
>Back to project</Link
|
||||||
|
>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="status"
|
v-if="status"
|
||||||
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
|
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 }}
|
{{ status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="grid gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]"
|
class="grid gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(320px,0.95fr)] 2xl:grid-cols-[minmax(0,1.35fr)_minmax(320px,0.95fr)_minmax(280px,0.85fr)]"
|
||||||
>
|
>
|
||||||
<Card>
|
<Card :class="frostedCardClass">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Ticket details</CardTitle>
|
<CardTitle
|
||||||
<CardDescription>
|
class="font-display dark:text-foreground text-2xl text-[#2d3335]"
|
||||||
|
>
|
||||||
|
Ticket details
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription class="max-w-2xl leading-6">
|
||||||
{{
|
{{
|
||||||
canEditTicket
|
canEditTicket
|
||||||
? 'Tracked ticket changes create worklog entries automatically.'
|
? 'Tracked ticket changes create worklog entries automatically.'
|
||||||
@@ -411,61 +558,104 @@ const formatDateTime = (value: string): string => {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<form class="grid gap-6" @submit.prevent="submit">
|
<form class="grid gap-6" @submit.prevent="submit">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="title">Title</Label>
|
<Label for="title" :class="fieldLabelClass">
|
||||||
|
Title
|
||||||
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="title"
|
id="title"
|
||||||
v-model="form.title"
|
v-model="form.title"
|
||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
|
:class="inputClass"
|
||||||
/>
|
/>
|
||||||
<InputError :message="form.errors.title" />
|
<InputError :message="form.errors.title" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2 md:grid-cols-2 md:gap-6">
|
<div class="grid gap-2 md:grid-cols-2 md:gap-6">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="ticket_type_id">Type</Label>
|
<Label
|
||||||
<select
|
for="ticket_type_id"
|
||||||
id="ticket_type_id"
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
|
Type
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
v-model="form.ticket_type_id"
|
v-model="form.ticket_type_id"
|
||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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
|
<SelectTrigger
|
||||||
v-for="ticketType in ticketTypes"
|
id="ticket_type_id"
|
||||||
:key="ticketType.id"
|
:class="selectTriggerClass"
|
||||||
:value="ticketType.id"
|
|
||||||
>
|
>
|
||||||
{{ ticketType.name }}
|
<SelectValue
|
||||||
</option>
|
placeholder="Choose a type"
|
||||||
</select>
|
/>
|
||||||
|
</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
|
<InputError
|
||||||
:message="form.errors.ticket_type_id"
|
:message="form.errors.ticket_type_id"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="ticket_priority_id">
|
<Label
|
||||||
|
for="ticket_priority_id"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
Priority
|
Priority
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<Select
|
||||||
id="ticket_priority_id"
|
|
||||||
v-model="form.ticket_priority_id"
|
v-model="form.ticket_priority_id"
|
||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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
|
<SelectTrigger
|
||||||
v-for="ticketPriority in ticketPriorities"
|
id="ticket_priority_id"
|
||||||
:key="ticketPriority.id"
|
:class="selectTriggerClass"
|
||||||
:value="ticketPriority.id"
|
|
||||||
>
|
>
|
||||||
{{ ticketPriority.name }}
|
<SelectValue
|
||||||
</option>
|
placeholder="Choose a priority"
|
||||||
</select>
|
/>
|
||||||
|
</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
|
<InputError
|
||||||
:message="
|
:message="
|
||||||
form.errors.ticket_priority_id
|
form.errors.ticket_priority_id
|
||||||
@@ -476,7 +666,10 @@ const formatDateTime = (value: string): string => {
|
|||||||
|
|
||||||
<div class="grid gap-2 md:grid-cols-2 md:gap-6">
|
<div class="grid gap-2 md:grid-cols-2 md:gap-6">
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="assignee_user_id">
|
<Label
|
||||||
|
for="assignee_user_id"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
Assignee
|
Assignee
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<select
|
||||||
@@ -485,7 +678,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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"
|
:class="nativeSelectClass"
|
||||||
>
|
>
|
||||||
<option value="">Unassigned</option>
|
<option value="">Unassigned</option>
|
||||||
<option
|
<option
|
||||||
@@ -502,14 +695,19 @@ const formatDateTime = (value: string): string => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="canViewAuthor" class="grid gap-2">
|
<div v-if="canViewAuthor" class="grid gap-2">
|
||||||
<Label for="author_user_id">Author</Label>
|
<Label
|
||||||
|
for="author_user_id"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
|
Author
|
||||||
|
</Label>
|
||||||
<select
|
<select
|
||||||
id="author_user_id"
|
id="author_user_id"
|
||||||
v-model="form.author_user_id"
|
v-model="form.author_user_id"
|
||||||
:disabled="
|
:disabled="
|
||||||
!canEditAuthor || form.processing
|
!canEditAuthor || form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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"
|
:class="nativeSelectClass"
|
||||||
>
|
>
|
||||||
<option value="">No author</option>
|
<option value="">No author</option>
|
||||||
<option
|
<option
|
||||||
@@ -530,7 +728,10 @@ const formatDateTime = (value: string): string => {
|
|||||||
class="grid gap-2"
|
class="grid gap-2"
|
||||||
v-if="project.show_story_points"
|
v-if="project.show_story_points"
|
||||||
>
|
>
|
||||||
<Label for="story_points">
|
<Label
|
||||||
|
for="story_points"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
Estimated story points
|
Estimated story points
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -539,6 +740,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
v-model="form.story_points"
|
v-model="form.story_points"
|
||||||
type="number"
|
type="number"
|
||||||
step="1"
|
step="1"
|
||||||
|
:class="inputClass"
|
||||||
/>
|
/>
|
||||||
<InputError
|
<InputError
|
||||||
:message="form.errors.story_points"
|
:message="form.errors.story_points"
|
||||||
@@ -546,7 +748,10 @@ const formatDateTime = (value: string): string => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="parent_ticket_id">
|
<Label
|
||||||
|
for="parent_ticket_id"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
Parent ticket
|
Parent ticket
|
||||||
</Label>
|
</Label>
|
||||||
<select
|
<select
|
||||||
@@ -555,7 +760,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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"
|
:class="nativeSelectClass"
|
||||||
>
|
>
|
||||||
<option value="">No parent ticket</option>
|
<option value="">No parent ticket</option>
|
||||||
<option
|
<option
|
||||||
@@ -574,15 +779,19 @@ const formatDateTime = (value: string): string => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="description">Description</Label>
|
<Label
|
||||||
<textarea
|
for="description"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
rows="5"
|
|
||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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"
|
:class="textareaClass"
|
||||||
/>
|
/>
|
||||||
<InputError
|
<InputError
|
||||||
:message="form.errors.description"
|
:message="form.errors.description"
|
||||||
@@ -590,17 +799,19 @@ const formatDateTime = (value: string): string => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="acceptance_criteria">
|
<Label
|
||||||
|
for="acceptance_criteria"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
Acceptance criteria
|
Acceptance criteria
|
||||||
</Label>
|
</Label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="acceptance_criteria"
|
id="acceptance_criteria"
|
||||||
v-model="form.acceptance_criteria"
|
v-model="form.acceptance_criteria"
|
||||||
rows="5"
|
|
||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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"
|
:class="textareaClass"
|
||||||
/>
|
/>
|
||||||
<InputError
|
<InputError
|
||||||
:message="form.errors.acceptance_criteria"
|
:message="form.errors.acceptance_criteria"
|
||||||
@@ -608,36 +819,48 @@ const formatDateTime = (value: string): string => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="border-border grid gap-3 rounded-xl border p-4"
|
class="dark:bg-muted/25 grid gap-4 rounded-[1.5rem] border border-dashed border-[#adb3b5]/35 bg-[#f8f9fa]/80 p-5 dark:border-white/10"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<input
|
<Checkbox
|
||||||
id="is_blocked"
|
id="is_blocked"
|
||||||
v-model="form.is_blocked"
|
:checked="form.is_blocked"
|
||||||
type="checkbox"
|
|
||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
class="border-input text-primary focus:ring-ring size-4 rounded"
|
@update:checked="
|
||||||
|
(
|
||||||
|
value:
|
||||||
|
| boolean
|
||||||
|
| 'indeterminate',
|
||||||
|
) =>
|
||||||
|
(form.is_blocked =
|
||||||
|
value === true)
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<Label for="is_blocked">
|
<Label
|
||||||
|
for="is_blocked"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
Mark ticket as blocked
|
Mark ticket as blocked
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="blocked_comment">
|
<Label
|
||||||
|
for="blocked_comment"
|
||||||
|
:class="fieldLabelClass"
|
||||||
|
>
|
||||||
Blocker comment
|
Blocker comment
|
||||||
</Label>
|
</Label>
|
||||||
<textarea
|
<Textarea
|
||||||
id="blocked_comment"
|
id="blocked_comment"
|
||||||
v-model="form.blocked_comment"
|
v-model="form.blocked_comment"
|
||||||
rows="3"
|
|
||||||
:disabled="
|
:disabled="
|
||||||
!canEditTicket || form.processing
|
!canEditTicket || form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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"
|
|
||||||
placeholder="Describe what currently blocks progress on this ticket."
|
placeholder="Describe what currently blocks progress on this ticket."
|
||||||
|
:class="textareaClass"
|
||||||
/>
|
/>
|
||||||
<InputError
|
<InputError
|
||||||
:message="form.errors.blocked_comment"
|
:message="form.errors.blocked_comment"
|
||||||
@@ -674,6 +897,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
>
|
>
|
||||||
<Label
|
<Label
|
||||||
:for="`field-${ticketFieldDefinition.id}`"
|
:for="`field-${ticketFieldDefinition.id}`"
|
||||||
|
:class="fieldLabelClass"
|
||||||
>
|
>
|
||||||
{{ ticketFieldDefinition.label }}
|
{{ ticketFieldDefinition.label }}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -692,7 +916,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
!canEditTicket ||
|
!canEditTicket ||
|
||||||
form.processing
|
form.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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"
|
:class="nativeSelectClass"
|
||||||
>
|
>
|
||||||
<option value="">Unassigned</option>
|
<option value="">Unassigned</option>
|
||||||
<option
|
<option
|
||||||
@@ -717,6 +941,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
!canEditTicket ||
|
!canEditTicket ||
|
||||||
form.processing
|
form.processing
|
||||||
"
|
"
|
||||||
|
:class="inputClass"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InputError
|
<InputError
|
||||||
@@ -749,7 +974,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<Card class="h-fit">
|
<Card :class="frostedAsideCardClass">
|
||||||
<CardHeader
|
<CardHeader
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
||||||
>
|
>
|
||||||
@@ -804,7 +1029,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
:disabled="
|
:disabled="
|
||||||
branchCreationForm.processing
|
branchCreationForm.processing
|
||||||
"
|
"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 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"
|
:class="nativeSelectClass"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="repositoryConnection in linkedRepositoryConnections"
|
v-for="repositoryConnection in linkedRepositoryConnections"
|
||||||
@@ -843,6 +1068,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
branchCreationForm.processing ||
|
branchCreationForm.processing ||
|
||||||
!selectedBranchRepositoryHasPersonalAccess
|
!selectedBranchRepositoryHasPersonalAccess
|
||||||
"
|
"
|
||||||
|
:class="inputClass"
|
||||||
/>
|
/>
|
||||||
<InputError
|
<InputError
|
||||||
:message="
|
:message="
|
||||||
@@ -1041,7 +1267,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card class="h-fit">
|
<Card :class="frostedAsideCardClass">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Linked tickets</CardTitle>
|
<CardTitle>Linked tickets</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -1057,13 +1283,14 @@ const formatDateTime = (value: string): string => {
|
|||||||
ticketLinkTypes.length > 0 &&
|
ticketLinkTypes.length > 0 &&
|
||||||
ticketLinkOptions.length > 0
|
ticketLinkOptions.length > 0
|
||||||
"
|
"
|
||||||
v-bind="
|
:action="
|
||||||
ProjectTicketLinkController.store.form({
|
ProjectTicketLinkController.store.url({
|
||||||
project: project.id,
|
project: project.id,
|
||||||
ticket: ticket.id,
|
ticket: ticket.id,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
class="border-border grid gap-4 rounded-xl border border-dashed p-4"
|
class="border-border grid gap-4 rounded-xl border border-dashed p-4"
|
||||||
|
method="post"
|
||||||
v-slot="{ errors, processing }"
|
v-slot="{ errors, processing }"
|
||||||
>
|
>
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
@@ -1073,7 +1300,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
<select
|
<select
|
||||||
id="ticket_link_type_id"
|
id="ticket_link_type_id"
|
||||||
name="ticket_link_type_id"
|
name="ticket_link_type_id"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
:class="nativeSelectClass"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="ticketLinkType in ticketLinkTypes"
|
v-for="ticketLinkType in ticketLinkTypes"
|
||||||
@@ -1095,7 +1322,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
<select
|
<select
|
||||||
id="linked_ticket_id"
|
id="linked_ticket_id"
|
||||||
name="linked_ticket_id"
|
name="linked_ticket_id"
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
:class="nativeSelectClass"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="ticketLinkOption in ticketLinkOptions"
|
v-for="ticketLinkOption in ticketLinkOptions"
|
||||||
@@ -1164,8 +1391,8 @@ const formatDateTime = (value: string): string => {
|
|||||||
|
|
||||||
<Form
|
<Form
|
||||||
v-if="canEditTicket"
|
v-if="canEditTicket"
|
||||||
v-bind="
|
:action="
|
||||||
ProjectTicketLinkController.destroy.form(
|
ProjectTicketLinkController.destroy.url(
|
||||||
{
|
{
|
||||||
project: project.id,
|
project: project.id,
|
||||||
ticket: ticket.id,
|
ticket: ticket.id,
|
||||||
@@ -1174,6 +1401,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
class="mt-3 flex justify-end"
|
class="mt-3 flex justify-end"
|
||||||
|
method="delete"
|
||||||
v-slot="{ processing: deleting }"
|
v-slot="{ processing: deleting }"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -1190,7 +1418,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card class="h-fit">
|
<Card :class="frostedAsideCardClass">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Pull requests</CardTitle>
|
<CardTitle>Pull requests</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -1319,7 +1547,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card class="h-fit">
|
<Card :class="frostedAsideCardClass">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Child tickets</CardTitle>
|
<CardTitle>Child tickets</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -1345,7 +1573,7 @@ const formatDateTime = (value: string): string => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card class="h-fit">
|
<Card :class="frostedAsideCardClass">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Worklog</CardTitle>
|
<CardTitle>Worklog</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -1402,6 +1630,108 @@ const formatDateTime = (value: string): string => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6 2xl:sticky 2xl:top-24 2xl:self-start">
|
||||||
|
<Card :class="frostedAsideCardClass">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle
|
||||||
|
class="font-display dark:text-foreground text-xl text-[#2d3335]"
|
||||||
|
>
|
||||||
|
Snapshot
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Quick context for the ticket without scrolling
|
||||||
|
through the full detail form.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent class="space-y-3">
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/35 rounded-[1.25rem] bg-[#f1f4f5] px-4 py-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.8)]"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Assignee
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-sm font-medium text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ selectedAssignee?.name ?? 'Unassigned' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Author
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-sm font-medium text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ selectedAuthor?.name ?? 'No author' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid gap-3 sm:grid-cols-2 2xl:grid-cols-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Branches
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-lg font-semibold text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ repositoryBranchCount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Pull requests
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-lg font-semibold text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ pullRequestCount }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark:bg-muted/25 rounded-[1.25rem] bg-[#f8f9fa] px-4 py-3"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="dark:text-muted-foreground text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-[#5a6062]"
|
||||||
|
>
|
||||||
|
Last updated
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="dark:text-foreground mt-1 text-sm font-medium text-[#2d3335]"
|
||||||
|
>
|
||||||
|
{{ formatDateTime(ticket.updated_at) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<FeatureComingSoonPanel
|
||||||
|
feature="Comments & discussion"
|
||||||
|
description="The Stitch design already reserves this full rail for threaded comments, collaboration, and follow-up context. The visual shell is in place here until the feature is wired."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Form, Head, Link } from '@inertiajs/vue3';
|
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||||
import ProjectTicketController from '@/actions/App/Http/Controllers/ProjectTicketController';
|
import { computed } from 'vue';
|
||||||
import Heading from '@/components/Heading.vue';
|
import TicketPriorityIcon from '@/components/TicketPriorityIcon.vue';
|
||||||
|
import TicketTypeIcon from '@/components/TicketTypeIcon.vue';
|
||||||
import InputError from '@/components/InputError.vue';
|
import InputError from '@/components/InputError.vue';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -14,6 +15,14 @@ import {
|
|||||||
} from '@/components/ui/card';
|
} from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
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 AppLayout from '@/layouts/AppLayout.vue';
|
||||||
import { index as backlogIndex } from '@/routes/projects/backlog';
|
import { index as backlogIndex } from '@/routes/projects/backlog';
|
||||||
import { index as boardIndex } from '@/routes/projects/board';
|
import { index as boardIndex } from '@/routes/projects/board';
|
||||||
@@ -24,11 +33,27 @@ import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
|||||||
|
|
||||||
const props = defineProps<ProjectTicketCreatePageProps>();
|
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 =
|
const returnHref =
|
||||||
props.source === 'board'
|
props.source === 'board'
|
||||||
? boardIndex(props.project.id)
|
? boardIndex(props.project.id)
|
||||||
: backlogIndex(props.project.id);
|
: backlogIndex(props.project.id);
|
||||||
const returnLabel = props.source === 'board' ? 'Back to board' : 'Back to backlog';
|
const returnLabel =
|
||||||
|
props.source === 'board' ? 'Back to board' : 'Back to backlog';
|
||||||
|
|
||||||
const breadcrumbs: BreadcrumbItem[] = [
|
const breadcrumbs: BreadcrumbItem[] = [
|
||||||
{
|
{
|
||||||
@@ -45,9 +70,7 @@ const breadcrumbs: BreadcrumbItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Create ticket',
|
title: 'Create ticket',
|
||||||
href: ProjectTicketController.create({
|
href: `/projects/${props.project.id}/tickets/create?source=${props.source}`,
|
||||||
project: props.project.id,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -56,6 +79,53 @@ const hasTicketMetadata =
|
|||||||
props.ticketPriorities.length > 0 &&
|
props.ticketPriorities.length > 0 &&
|
||||||
props.ticketStatusOptions.length > 0;
|
props.ticketStatusOptions.length > 0;
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
acceptance_criteria: '',
|
||||||
|
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 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.post(`/projects/${props.project.id}/tickets`, {
|
||||||
|
preserveScroll: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
||||||
if (
|
if (
|
||||||
ticketStatus.board_column === null ||
|
ticketStatus.board_column === null ||
|
||||||
@@ -76,226 +146,543 @@ const ticketStatusLabel = (ticketStatus: TicketStatusModel): string => {
|
|||||||
<AppLayout :breadcrumbs="breadcrumbs">
|
<AppLayout :breadcrumbs="breadcrumbs">
|
||||||
<Head :title="`Create ${project.name} ticket`" />
|
<Head :title="`Create ${project.name} ticket`" />
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col gap-6 p-4">
|
<div class="relative isolate flex flex-1 flex-col gap-8 px-4 py-6">
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
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%)]"
|
||||||
>
|
/>
|
||||||
<div class="space-y-3">
|
|
||||||
<Badge variant="outline" class="px-3 py-1">
|
|
||||||
{{ project.key }}
|
|
||||||
</Badge>
|
|
||||||
<Heading
|
|
||||||
title="Create ticket"
|
|
||||||
:description="`Add a ticket to ${project.name}.`"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-3">
|
<section
|
||||||
<Button as-child variant="outline">
|
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"
|
||||||
<Link :href="returnHref">{{ returnLabel }}</Link>
|
>
|
||||||
</Button>
|
<div
|
||||||
<Button as-child variant="outline">
|
class="flex flex-col gap-6 xl:flex-row xl:items-start xl:justify-between"
|
||||||
<Link :href="show(project.id)">Back to project</Link>
|
>
|
||||||
</Button>
|
<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>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="status"
|
v-if="status"
|
||||||
class="rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-900 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100"
|
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 }}
|
{{ status }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card v-if="hasTicketMetadata">
|
<div
|
||||||
<CardHeader>
|
v-if="hasTicketMetadata"
|
||||||
<CardTitle>Ticket details</CardTitle>
|
class="grid gap-6 xl:grid-cols-[minmax(0,1.45fr)_22rem]"
|
||||||
<CardDescription>
|
>
|
||||||
The ticket key is assigned after creation.
|
<Card :class="frostedCardClass">
|
||||||
</CardDescription>
|
<CardHeader class="border-b border-[#adb3b5]/20 pb-6">
|
||||||
</CardHeader>
|
<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>
|
<CardContent class="p-6">
|
||||||
<Form
|
<form class="grid gap-6" @submit.prevent="submit">
|
||||||
v-bind="ProjectTicketController.store.form(project.id)"
|
<div class="grid gap-2">
|
||||||
class="grid gap-6"
|
<Label for="title" :class="fieldLabelClass">
|
||||||
v-slot="{ errors, processing }"
|
Title
|
||||||
>
|
</Label>
|
||||||
<div class="grid gap-2">
|
<Input
|
||||||
<Label for="title">Title</Label>
|
id="title"
|
||||||
<Input
|
v-model="form.title"
|
||||||
id="title"
|
required
|
||||||
name="title"
|
autofocus
|
||||||
required
|
:disabled="form.processing"
|
||||||
autofocus
|
placeholder="Implement repository sync"
|
||||||
placeholder="Implement repository sync"
|
:class="inputClass"
|
||||||
/>
|
/>
|
||||||
<InputError :message="errors.title" />
|
<InputError :message="form.errors.title" />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_20rem]">
|
|
||||||
<div class="grid gap-6">
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label for="description">Description</Label>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
rows="4"
|
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
||||||
placeholder="Describe the expected behavior and relevant context."
|
|
||||||
/>
|
|
||||||
<InputError :message="errors.description" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label for="acceptance_criteria">
|
|
||||||
Acceptance criteria
|
|
||||||
</Label>
|
|
||||||
<textarea
|
|
||||||
id="acceptance_criteria"
|
|
||||||
name="acceptance_criteria"
|
|
||||||
rows="4"
|
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
||||||
placeholder="List the conditions that must be true before this ticket is done."
|
|
||||||
/>
|
|
||||||
<InputError
|
|
||||||
:message="errors.acceptance_criteria"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid h-fit gap-6">
|
<div
|
||||||
<div class="grid gap-2">
|
class="grid gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]"
|
||||||
<Label for="ticket_type_id">Type</Label>
|
>
|
||||||
<select
|
<div class="grid gap-6">
|
||||||
id="ticket_type_id"
|
<div class="grid gap-2">
|
||||||
name="ticket_type_id"
|
<Label
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
for="description"
|
||||||
>
|
:class="fieldLabelClass"
|
||||||
<option
|
|
||||||
v-for="ticketType in ticketTypes"
|
|
||||||
:key="ticketType.id"
|
|
||||||
:value="ticketType.id"
|
|
||||||
>
|
>
|
||||||
{{ ticketType.name }}
|
Description
|
||||||
</option>
|
</Label>
|
||||||
</select>
|
<Textarea
|
||||||
<InputError
|
id="description"
|
||||||
:message="errors.ticket_type_id"
|
v-model="form.description"
|
||||||
/>
|
:disabled="form.processing"
|
||||||
</div>
|
placeholder="Describe the expected behavior, context, and why this work matters."
|
||||||
|
:class="textareaClass"
|
||||||
|
/>
|
||||||
|
<InputError
|
||||||
|
:message="form.errors.description"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-2">
|
<div class="grid gap-2">
|
||||||
<Label for="ticket_priority_id">
|
<Label
|
||||||
Priority
|
for="acceptance_criteria"
|
||||||
</Label>
|
:class="fieldLabelClass"
|
||||||
<select
|
|
||||||
id="ticket_priority_id"
|
|
||||||
name="ticket_priority_id"
|
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="ticketPriority in ticketPriorities"
|
|
||||||
:key="ticketPriority.id"
|
|
||||||
:value="ticketPriority.id"
|
|
||||||
>
|
>
|
||||||
{{ ticketPriority.name }}
|
Acceptance criteria
|
||||||
</option>
|
</Label>
|
||||||
</select>
|
<Textarea
|
||||||
<InputError
|
id="acceptance_criteria"
|
||||||
:message="errors.ticket_priority_id"
|
v-model="form.acceptance_criteria"
|
||||||
/>
|
:disabled="form.processing"
|
||||||
</div>
|
placeholder="List the conditions that must be true before this ticket is done."
|
||||||
|
:class="textareaClass"
|
||||||
<div class="grid gap-2">
|
/>
|
||||||
<Label for="ticket_status_id">Status</Label>
|
<InputError
|
||||||
<select
|
:message="
|
||||||
id="ticket_status_id"
|
form.errors.acceptance_criteria
|
||||||
name="ticket_status_id"
|
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="ticketStatus in ticketStatusOptions"
|
|
||||||
:key="ticketStatus.id"
|
|
||||||
:value="ticketStatus.id"
|
|
||||||
:selected="
|
|
||||||
ticketStatus.id ===
|
|
||||||
defaultTicketStatusId
|
|
||||||
"
|
"
|
||||||
>
|
/>
|
||||||
{{ ticketStatusLabel(ticketStatus) }}
|
</div>
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<InputError
|
|
||||||
:message="errors.ticket_status_id"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid gap-2">
|
|
||||||
<Label for="assignee_user_id">
|
|
||||||
Assignee
|
|
||||||
</Label>
|
|
||||||
<select
|
|
||||||
id="assignee_user_id"
|
|
||||||
name="assignee_user_id"
|
|
||||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px]"
|
|
||||||
>
|
|
||||||
<option value="">Unassigned</option>
|
|
||||||
<option
|
|
||||||
v-for="user in users"
|
|
||||||
:key="user.id"
|
|
||||||
:value="user.id"
|
|
||||||
>
|
|
||||||
{{ user.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<InputError
|
|
||||||
:message="errors.assignee_user_id"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="project.show_story_points"
|
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)]"
|
||||||
class="grid gap-2"
|
|
||||||
>
|
>
|
||||||
<Label for="story_points">
|
<div class="grid gap-2">
|
||||||
Estimated story points
|
<Label
|
||||||
</Label>
|
for="ticket_type_id"
|
||||||
<Input
|
:class="fieldLabelClass"
|
||||||
id="story_points"
|
>
|
||||||
name="story_points"
|
Type
|
||||||
type="number"
|
</Label>
|
||||||
step="1"
|
<Select
|
||||||
/>
|
v-model="form.ticket_type_id"
|
||||||
<InputError
|
:disabled="form.processing"
|
||||||
:message="errors.story_points"
|
>
|
||||||
/>
|
<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>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3 pt-2">
|
||||||
<Button type="submit" :disabled="processing">
|
<Button
|
||||||
Create ticket
|
type="submit"
|
||||||
</Button>
|
:disabled="form.processing"
|
||||||
<Button as-child variant="outline">
|
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"
|
||||||
<Link :href="returnHref">
|
>
|
||||||
{{ returnLabel }}
|
Create ticket
|
||||||
</Link>
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
</div>
|
as-child
|
||||||
</Form>
|
variant="outline"
|
||||||
</CardContent>
|
:class="airyOutlineButtonClass"
|
||||||
</Card>
|
>
|
||||||
|
<Link :href="returnHref">
|
||||||
|
{{ returnLabel }}
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card v-else>
|
<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">
|
<CardContent class="flex flex-col gap-4 p-6 text-sm">
|
||||||
<p class="text-muted-foreground">
|
<p
|
||||||
|
class="dark:text-muted-foreground leading-7 text-[#5a6062]"
|
||||||
|
>
|
||||||
Ticket creation needs at least one type, one priority,
|
Ticket creation needs at least one type, one priority,
|
||||||
and one status.
|
and one status.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<Button as-child variant="outline">
|
<Button
|
||||||
|
as-child
|
||||||
|
variant="outline"
|
||||||
|
:class="airyOutlineButtonClass"
|
||||||
|
>
|
||||||
<Link :href="metadataSettings(project.id)">
|
<Link :href="metadataSettings(project.id)">
|
||||||
Open ticket settings
|
Open ticket settings
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
Reference in New Issue
Block a user