feat(ui) : add task list view with bulk actions, filters, and priority flag
- Add TaskListItem component with checkbox, project color, priority flag - Add TaskBulkActions toolbar (bulk status/user/priority/effort/group update, delete) - Add list view toggle button in my-tasks and project pages - Add Priorité and Effort filters to project page - TaskCard supports showProjectColor prop (color in my-tasks, neutral in project) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,16 @@
|
||||
<span class="hidden sm:inline">+ Ajouter un ticket</span>
|
||||
<span class="sm:hidden">+ Ticket</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center justify-center rounded-md border p-1.5 transition-colors"
|
||||
:class="viewMode === 'list'
|
||||
? 'border-primary-500 bg-primary-500 text-white'
|
||||
: 'border-primary-500 text-primary-500 hover:bg-primary-50'"
|
||||
title="Vue liste"
|
||||
@click="viewMode = viewMode === 'kanban' ? 'list' : 'kanban'"
|
||||
>
|
||||
<Icon name="mdi:format-list-bulleted" size="20" />
|
||||
</button>
|
||||
<button
|
||||
class="flex shrink-0 items-center rounded-md bg-neutral-200 px-3 py-2 text-neutral-600 hover:bg-neutral-300 sm:px-4"
|
||||
title="Paramètres du projet"
|
||||
@@ -58,11 +68,29 @@
|
||||
text-field="text-sm"
|
||||
text-value="text-sm"
|
||||
/>
|
||||
<MalioSelect
|
||||
v-model="selectedPriorityId"
|
||||
:options="priorityFilterOptions"
|
||||
label="Priorité"
|
||||
empty-option-label="Toutes"
|
||||
min-width="!w-40"
|
||||
text-field="text-sm"
|
||||
text-value="text-sm"
|
||||
/>
|
||||
<MalioSelect
|
||||
v-model="selectedEffortId"
|
||||
:options="effortFilterOptions"
|
||||
label="Effort"
|
||||
empty-option-label="Tous"
|
||||
min-width="!w-40"
|
||||
text-field="text-sm"
|
||||
text-value="text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kanban -->
|
||||
<div class="mt-6 flex h-[calc(100vh-200px)] gap-3 overflow-x-auto pb-4">
|
||||
<div v-if="viewMode === 'kanban'" class="mt-6 flex h-[calc(100vh-200px)] gap-3 overflow-x-auto pb-4">
|
||||
<div
|
||||
v-for="status in statuses"
|
||||
:key="status.id"
|
||||
@@ -100,6 +128,7 @@
|
||||
|
||||
<!-- Backlog -->
|
||||
<div
|
||||
v-if="viewMode === 'kanban'"
|
||||
class="mt-8 rounded-lg p-4 transition-colors"
|
||||
:class="dragOverStatusId === 0 ? 'bg-tertiary-600' : 'bg-tertiary-500'"
|
||||
@dragover.prevent
|
||||
@@ -118,6 +147,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div v-if="viewMode === 'list'" class="mt-6 flex flex-col gap-2.5 rounded-[10px] bg-tertiary-500 p-2.5">
|
||||
<TaskBulkActions
|
||||
:selected-count="selectedTaskIds.size"
|
||||
:total-count="filteredTasks.length"
|
||||
:all-selected="filteredTasks.length > 0 && selectedTaskIds.size === filteredTasks.length"
|
||||
:some-selected="selectedTaskIds.size > 0 && selectedTaskIds.size < filteredTasks.length"
|
||||
:statuses="statuses"
|
||||
:users="users"
|
||||
:priorities="priorities"
|
||||
:efforts="efforts"
|
||||
:groups="groups"
|
||||
@toggle-all="toggleSelectAll(filteredTasks)"
|
||||
@bulk-update="onBulkUpdate"
|
||||
@bulk-archive="onBulkArchive"
|
||||
@bulk-delete="onBulkDelete"
|
||||
/>
|
||||
<TaskListItem
|
||||
v-for="task in filteredTasks"
|
||||
:key="task.id"
|
||||
:task="task"
|
||||
:selected="selectedTaskIds.has(task.id)"
|
||||
@click="openTaskEdit(task)"
|
||||
@toggle-select="toggleTaskSelect"
|
||||
/>
|
||||
<p
|
||||
v-if="filteredTasks.length === 0"
|
||||
class="py-8 text-center text-sm text-neutral-400"
|
||||
>
|
||||
Aucun ticket
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TaskModal
|
||||
v-model="taskDrawerOpen"
|
||||
:task="selectedTask"
|
||||
@@ -191,6 +253,10 @@ const selectedGroupId = ref<number | null>(null)
|
||||
const selectedTagId = ref<number | null>(null)
|
||||
const selectedAssigneeId = ref<number | null>(null)
|
||||
const selectedStatusId = ref<number | null>(null)
|
||||
const selectedPriorityId = ref<number | null>(null)
|
||||
const selectedEffortId = ref<number | null>(null)
|
||||
const viewMode = ref<'kanban' | 'list'>('kanban')
|
||||
const selectedTaskIds = reactive(new Set<number>())
|
||||
const dragOverStatusId = ref<number | null>(null)
|
||||
const dragCounter = ref(0)
|
||||
const taskDrawerOpen = ref(false)
|
||||
@@ -213,6 +279,14 @@ const statusFilterOptions = computed(() =>
|
||||
statuses.value.map(s => ({ label: s.label, value: s.id }))
|
||||
)
|
||||
|
||||
const priorityFilterOptions = computed(() =>
|
||||
priorities.value.map(p => ({ label: p.label, value: p.id }))
|
||||
)
|
||||
|
||||
const effortFilterOptions = computed(() =>
|
||||
efforts.value.map(e => ({ label: e.label, value: e.id }))
|
||||
)
|
||||
|
||||
const filteredTasks = computed(() => {
|
||||
let result = tasks.value.filter(t => !t.archived)
|
||||
if (selectedGroupId.value) {
|
||||
@@ -227,6 +301,12 @@ const filteredTasks = computed(() => {
|
||||
if (selectedStatusId.value) {
|
||||
result = result.filter(t => t.status?.id === selectedStatusId.value)
|
||||
}
|
||||
if (selectedPriorityId.value) {
|
||||
result = result.filter(t => t.priority?.id === selectedPriorityId.value)
|
||||
}
|
||||
if (selectedEffortId.value) {
|
||||
result = result.filter(t => t.effort?.id === selectedEffortId.value)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
@@ -311,6 +391,52 @@ async function onDropBacklog(event: DragEvent) {
|
||||
await taskService.update(taskId, { status: null })
|
||||
}
|
||||
|
||||
function toggleTaskSelect(taskId: number) {
|
||||
if (selectedTaskIds.has(taskId)) {
|
||||
selectedTaskIds.delete(taskId)
|
||||
} else {
|
||||
selectedTaskIds.add(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll(taskList: Task[]) {
|
||||
if (selectedTaskIds.size === taskList.length) {
|
||||
selectedTaskIds.clear()
|
||||
} else {
|
||||
taskList.forEach(t => selectedTaskIds.add(t.id))
|
||||
}
|
||||
}
|
||||
|
||||
async function onBulkUpdate(field: string, value: number) {
|
||||
const ids = [...selectedTaskIds]
|
||||
if (ids.length === 0) return
|
||||
const payload: Record<string, unknown> = {}
|
||||
if (field === 'status') payload.status = `/api/task_statuses/${value}`
|
||||
else if (field === 'assignee') payload.assignee = `/api/users/${value}`
|
||||
else if (field === 'priority') payload.priority = `/api/task_priorities/${value}`
|
||||
else if (field === 'effort') payload.effort = `/api/task_efforts/${value}`
|
||||
else if (field === 'group') payload.group = `/api/task_groups/${value}`
|
||||
await Promise.all(ids.map(id => taskService.update(id, payload)))
|
||||
selectedTaskIds.clear()
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function onBulkArchive() {
|
||||
const ids = [...selectedTaskIds]
|
||||
if (ids.length === 0) return
|
||||
await Promise.all(ids.map(id => taskService.update(id, { archived: true })))
|
||||
selectedTaskIds.clear()
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function onBulkDelete() {
|
||||
const ids = [...selectedTaskIds]
|
||||
if (ids.length === 0) return
|
||||
await Promise.all(ids.map(id => taskService.remove(id)))
|
||||
selectedTaskIds.clear()
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function onSaved() {
|
||||
await loadData()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user