Implement Fluid Emerald app redesign
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, Head, Link } from '@inertiajs/vue3';
|
||||
import { ArrowRight, CircleUserRound } from 'lucide-vue-next';
|
||||
import ProjectBoardController from '@/actions/App/Http/Controllers/ProjectBoardController';
|
||||
import Heading from '@/components/Heading.vue';
|
||||
import UserAvatar from '@/components/ui/avatar/UserAvatar.vue';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -16,7 +18,10 @@ import { index as backlogIndex } from '@/routes/projects/backlog';
|
||||
import { index as projectIndex, show } from '@/routes/projects/index';
|
||||
import { board as boardSettings } from '@/routes/projects/settings';
|
||||
import { index as boardIndex } from '@/routes/projects/board';
|
||||
import { create as createTicket, show as showTicket } from '@/routes/projects/tickets';
|
||||
import {
|
||||
create as createTicket,
|
||||
show as showTicket,
|
||||
} from '@/routes/projects/tickets';
|
||||
import type { BreadcrumbItem, ProjectBoardPageProps } from '@/types';
|
||||
import type { TicketModel } from '@/types/models/ticket-model';
|
||||
import type { TicketStatusModel } from '@/types/models/ticket-status-model';
|
||||
@@ -65,10 +70,10 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<AppLayout :breadcrumbs="breadcrumbs" :project="project">
|
||||
<Head :title="`${project.name} board`" />
|
||||
|
||||
<div class="flex flex-1 flex-col gap-6 p-4">
|
||||
<div class="app-page max-w-none">
|
||||
<div
|
||||
class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
||||
>
|
||||
@@ -108,10 +113,7 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<div v-if="status" class="status-success">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
@@ -131,13 +133,18 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div v-else class="grid gap-4 xl:grid-cols-3">
|
||||
<div
|
||||
v-else
|
||||
class="-mx-4 flex snap-x snap-proximity items-start gap-4 overflow-x-auto px-4 pb-5 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8"
|
||||
>
|
||||
<Card
|
||||
v-for="boardColumn in boardColumns"
|
||||
:key="boardColumn.id"
|
||||
class="flex h-full flex-col gap-0 overflow-hidden"
|
||||
class="bg-surface-container-low w-[min(21rem,calc(100vw-3rem))] shrink-0 snap-start gap-0 overflow-hidden border-0 py-0 shadow-none hover:shadow-none sm:w-[21rem]"
|
||||
>
|
||||
<CardHeader class="bg-muted/20 border-b">
|
||||
<CardHeader
|
||||
class="border-outline-variant/60 sticky top-0 z-10 border-b px-4 py-4"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle>{{ boardColumn.name }}</CardTitle>
|
||||
@@ -163,33 +170,38 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
</div>
|
||||
|
||||
<Badge variant="secondary">
|
||||
#{{ boardColumn.position }}
|
||||
{{ boardColumn.tickets?.length ?? 0 }}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent class="flex flex-1 flex-col gap-4 p-4">
|
||||
<CardContent
|
||||
class="flex min-h-48 flex-1 flex-col gap-3 p-3"
|
||||
>
|
||||
<div
|
||||
v-if="(boardColumn.tickets?.length ?? 0) === 0"
|
||||
class="border-border text-muted-foreground rounded-xl border border-dashed px-4 py-6 text-sm"
|
||||
class="empty-state bg-transparent"
|
||||
>
|
||||
No tickets are currently in this column.
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
<div v-else class="flex flex-col gap-3">
|
||||
<Card
|
||||
v-for="ticket in boardColumn.tickets"
|
||||
:key="ticket.id"
|
||||
:class="[
|
||||
'gap-0',
|
||||
'shadow-xs gap-0 py-0 hover:-translate-y-0.5',
|
||||
ticket.is_blocked
|
||||
? 'border-amber-300 bg-amber-50/70 dark:border-amber-700/60 dark:bg-amber-950/30'
|
||||
: '',
|
||||
]"
|
||||
>
|
||||
<CardHeader class="pb-4">
|
||||
<CardHeader class="gap-3 px-4 pb-3 pt-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-primary"
|
||||
>
|
||||
{{ ticket.ticket_key }}
|
||||
</Badge>
|
||||
<Badge
|
||||
@@ -201,16 +213,6 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
<Badge variant="secondary">
|
||||
{{ ticket.ticket_type.name }}
|
||||
</Badge>
|
||||
<Badge variant="secondary">
|
||||
{{ ticket.ticket_priority.name }}
|
||||
</Badge>
|
||||
<Badge variant="outline">
|
||||
{{
|
||||
ticketStatusLabel(
|
||||
ticket.ticket_status,
|
||||
)
|
||||
}}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardTitle class="text-base">
|
||||
<Link
|
||||
@@ -240,9 +242,56 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
>
|
||||
{{ ticket.blocked_comment }}
|
||||
</CardDescription>
|
||||
|
||||
<div
|
||||
class="border-outline-variant/60 flex items-center justify-between gap-3 border-t pt-3"
|
||||
>
|
||||
<div
|
||||
class="flex min-w-0 items-center gap-2"
|
||||
>
|
||||
<UserAvatar
|
||||
v-if="ticket.assignee"
|
||||
:user="ticket.assignee"
|
||||
class="size-7"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="border-outline-variant text-muted-foreground flex size-7 items-center justify-center rounded-full border border-dashed"
|
||||
>
|
||||
<CircleUserRound
|
||||
class="size-4"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="text-muted-foreground truncate text-xs"
|
||||
>
|
||||
{{
|
||||
ticket.assignee?.name ??
|
||||
'Unassigned'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex shrink-0 items-center gap-1.5"
|
||||
>
|
||||
<span
|
||||
class="bg-primary size-1.5 rounded-full"
|
||||
/>
|
||||
<span
|
||||
class="text-muted-foreground text-xs font-medium"
|
||||
>
|
||||
{{
|
||||
ticket.ticket_priority.name
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent v-if="canEditTickets" class="pt-0">
|
||||
<CardContent
|
||||
v-if="canEditTickets"
|
||||
class="border-outline-variant/60 border-t px-4 py-3"
|
||||
>
|
||||
<Form
|
||||
v-bind="
|
||||
ProjectBoardController.move.form({
|
||||
@@ -258,15 +307,7 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
name="redirect_to"
|
||||
value="board"
|
||||
/>
|
||||
<label
|
||||
:for="`ticket-status-${ticket.id}`"
|
||||
class="text-muted-foreground text-xs font-medium uppercase tracking-wide"
|
||||
>
|
||||
Move ticket
|
||||
</label>
|
||||
<div
|
||||
class="flex flex-col gap-2 sm:flex-row"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<select
|
||||
:id="`ticket-status-${ticket.id}`"
|
||||
name="ticket_status_id"
|
||||
@@ -275,7 +316,7 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
ticket,
|
||||
).length === 0 || processing
|
||||
"
|
||||
class="border-input bg-background shadow-xs focus-visible:border-ring focus-visible:ring-ring/50 min-w-44 rounded-md border px-3 py-2 text-sm outline-none transition-[color,box-shadow] focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="min-w-0 flex-1 text-xs"
|
||||
>
|
||||
<option
|
||||
v-if="
|
||||
@@ -303,7 +344,8 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
</select>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
:disabled="
|
||||
processing ||
|
||||
allowedTargetStatuses(
|
||||
@@ -311,7 +353,10 @@ const allowedTargetStatuses = (ticket: TicketModel): TicketStatusModel[] => {
|
||||
).length === 0
|
||||
"
|
||||
>
|
||||
Update status
|
||||
<ArrowRight class="size-4" />
|
||||
<span class="sr-only"
|
||||
>Update status</span
|
||||
>
|
||||
</Button>
|
||||
</div>
|
||||
<p
|
||||
|
||||
Reference in New Issue
Block a user