ref: app UI
This commit is contained in:
@@ -1,49 +1,55 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
label="List"
|
||||
icon="pi pi-list"
|
||||
severity="secondary"
|
||||
outlined
|
||||
:to="{ name: 'tasks' }"
|
||||
/>
|
||||
<Button label="Board" icon="pi pi-th-large" severity="secondary" outlined :to="{ name: 'tasks-board' }" />
|
||||
</div>
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="mb-3 flex items-center justify-end">
|
||||
<Button v-if="auth.can('tasks', 'create')" label="New Task" icon="pi pi-plus" @click="openCreate" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4 overflow-x-auto pb-4">
|
||||
<div class="board-container flex min-h-0 flex-1 overflow-x-auto rounded-2xl border" style="border-color: var(--hairline); background: var(--panel)">
|
||||
<div
|
||||
v-for="col in columns"
|
||||
v-for="(col, i) in columns"
|
||||
:key="col.status"
|
||||
class="min-w-[260px] flex-1 rounded-2xl p-2" style="background: var(--canvas)"
|
||||
@dragover.prevent="dragOverStatus = col.status"
|
||||
@dragleave="dragOverStatus = null"
|
||||
class="board-col flex min-w-[260px] flex-1 flex-col p-3"
|
||||
:class="{ 'drag-over': dragOverStatus === col.status }"
|
||||
:style="i > 0 ? 'border-left: 1px solid var(--hairline)' : ''"
|
||||
@dragover.prevent="onDragOver(col.status, $event)"
|
||||
@dragleave="onDragLeave(col.status, $event)"
|
||||
@drop.prevent="onDrop(col.status)"
|
||||
>
|
||||
<div class="flex items-center justify-between px-3 py-2 font-semibold">
|
||||
<span>{{ col.label }}</span>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<Tag :value="col.label" :severity="col.severity" />
|
||||
<Tag :value="tasksIn(col.status).length" severity="secondary" />
|
||||
</div>
|
||||
<div
|
||||
v-for="task in tasksIn(col.status)"
|
||||
:key="task.id"
|
||||
class="task-card mb-2 cursor-pointer rounded-xl border p-3"
|
||||
:style="{ background: 'var(--panel)', borderColor: 'var(--hairline)' }"
|
||||
:class="{ 'opacity-40': draggingId === task.id }"
|
||||
:draggable="auth.can('tasks', 'edit')"
|
||||
@dragstart="onDragStart(task)"
|
||||
@dragend="onDragEnd"
|
||||
@click="openEdit(task)"
|
||||
>
|
||||
<div style="font-weight: 500">{{ task.title }}</div>
|
||||
<div v-if="task.description" class="muted-note mt-1 truncate">{{ task.description }}</div>
|
||||
<div class="mt-2 flex items-center gap-2 text-[0.8rem]">
|
||||
<Tag :value="task.priority" :severity="prioritySeverity(task.priority)" />
|
||||
<span class="muted-note">{{ task.assigneeName ?? 'Unassigned' }}</span>
|
||||
<span v-if="task.dueDate" class="muted-note">{{ formatDate(task.dueDate) }}</span>
|
||||
<div class="flex flex-1 flex-col gap-2">
|
||||
<div
|
||||
v-for="task in tasksIn(col.status)"
|
||||
:key="task.id"
|
||||
class="task-card cursor-pointer rounded-xl border p-3 shadow-sm"
|
||||
:style="{ background: 'var(--canvas)', borderColor: 'var(--hairline)' }"
|
||||
:class="{ 'opacity-40': draggingId === task.id }"
|
||||
draggable="true"
|
||||
@dragstart="onDragStart(task)"
|
||||
@dragend="onDragEnd"
|
||||
@click.stop="toggleDesc(task.id)"
|
||||
>
|
||||
<div class="font-medium">{{ task.title }}</div>
|
||||
<div v-if="expandedDescs.has(task.id) && task.description" class="muted-note mt-1 line-clamp-2 text-[0.85rem]">{{ task.description }}</div>
|
||||
<div class="mt-2 text-[0.8rem]" v-if="task.assigneeName">
|
||||
<span class="flex items-center gap-1 muted-note">
|
||||
<i class="pi pi-user text-[0.7rem]"></i> {{ task.assigneeName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[0.8rem]" v-if="task.dueDate">
|
||||
<span class="flex items-center gap-1 muted-note">
|
||||
<i class="pi pi-calendar text-[0.7rem]"></i> {{ formatDate(task.dueDate) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="tasksIn(col.status).length === 0 && dragOverStatus === col.status"
|
||||
class="flex flex-1 items-center justify-center rounded-xl text-sm"
|
||||
style="border: 2px dashed var(--primary); color: var(--primary)"
|
||||
>
|
||||
Drop here
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,7 +74,7 @@ import { getTasks, updateTask } from '../../services/modules'
|
||||
import { getMembers } from '../../services/backend'
|
||||
import { errorMessage } from '../../services/api'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import type { Task, TaskPriority, TaskStatus } from '../../types'
|
||||
import type { Task, TaskStatus } from '../../types'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
@@ -83,12 +89,20 @@ const dragOverStatus = ref<TaskStatus | null>(null)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const editingTask = ref<Task | null>(null)
|
||||
const expandedDescs = ref(new Set<string>())
|
||||
|
||||
function toggleDesc(id: string) {
|
||||
const s = new Set(expandedDescs.value)
|
||||
if (s.has(id)) s.delete(id)
|
||||
else s.add(id)
|
||||
expandedDescs.value = s
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{ status: 'Todo' as TaskStatus, label: 'To Do' },
|
||||
{ status: 'InProgress' as TaskStatus, label: 'In Progress' },
|
||||
{ status: 'Done' as TaskStatus, label: 'Done' },
|
||||
{ status: 'Cancelled' as TaskStatus, label: 'Cancelled' },
|
||||
{ status: 'Todo' as TaskStatus, label: 'To Do', severity: 'secondary' as const },
|
||||
{ status: 'InProgress' as TaskStatus, label: 'In Progress', severity: 'info' as const },
|
||||
{ status: 'Done' as TaskStatus, label: 'Done', severity: 'success' as const },
|
||||
{ status: 'Cancelled' as TaskStatus, label: 'Cancelled', severity: 'danger' as const },
|
||||
]
|
||||
|
||||
function tasksIn(status: TaskStatus) {
|
||||
@@ -125,11 +139,22 @@ function onDragEnd() {
|
||||
dragOverStatus.value = null
|
||||
}
|
||||
|
||||
function onDragOver(status: TaskStatus, e: DragEvent) {
|
||||
e.dataTransfer!.dropEffect = 'move'
|
||||
dragOverStatus.value = status
|
||||
}
|
||||
|
||||
function onDragLeave(_status: TaskStatus, e: DragEvent) {
|
||||
const related = e.relatedTarget as HTMLElement | null
|
||||
if (related && (e.currentTarget as HTMLElement).contains(related)) return
|
||||
if (dragOverStatus.value === _status) dragOverStatus.value = null
|
||||
}
|
||||
|
||||
async function onDrop(status: TaskStatus) {
|
||||
const id = draggingId.value
|
||||
dragOverStatus.value = null
|
||||
draggingId.value = null
|
||||
if (!id) return
|
||||
if (!id || !auth.can('tasks', 'edit')) return
|
||||
const task = tasks.value.find((t) => t.id === id)
|
||||
if (!task || task.status === status) return
|
||||
try {
|
||||
@@ -153,11 +178,6 @@ function openCreate() {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(task: Task) {
|
||||
editingTask.value = task
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function onSaved() {
|
||||
dialogVisible.value = false
|
||||
void load()
|
||||
@@ -168,10 +188,6 @@ function onDeleted() {
|
||||
void load()
|
||||
}
|
||||
|
||||
function prioritySeverity(p: TaskPriority) {
|
||||
return p === 'High' ? 'danger' : p === 'Medium' ? 'warn' : 'secondary'
|
||||
}
|
||||
|
||||
function statusLabel(s: TaskStatus) {
|
||||
return s.replace(/([A-Z])/g, ' $1').trim()
|
||||
}
|
||||
@@ -190,4 +206,9 @@ onMounted(async () => {
|
||||
.task-card:hover {
|
||||
border-color: var(--primary) !important;
|
||||
}
|
||||
.board-col.drag-over {
|
||||
background: color-mix(in srgb, var(--primary) 6%, transparent);
|
||||
box-shadow: inset 0 0 0 2px var(--primary);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user