To achieve the desired functionality of making a task row expandable to show its subtasks, you need to ensure that the subtasks are displayed correctly when a task row is expanded. Your current implementation is almost correct, but there are a few adjustments needed to make it work seamlessly.
Here's the updated code with the necessary changes:
<template>
<div class="section-wrapper">
<v-card-actions class="py-0 mb-1">
<v-breadcrumbs class="pl-0" :items="bread"></v-breadcrumbs>
</v-card-actions>
<v-row>
<v-col cols="12">
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<div>
<span class="title">Tasks</span>
</div>
<div class="d-flex align-center">
<v-radio-group v-model="filter" row>
<v-radio label="All Tasks" value="all" />
<v-radio label="My Tasks" value="my" />
</v-radio-group>
<v-text-field v-model="search" label="Tasks" prepend-icon="mdi-magnify" single-line hide-details></v-text-field>
<v-btn color="primary" @click="openAddModal" class="ml-3">Add Task</v-btn>
<v-btn icon @click="reloadTasks" class="ml-2">
<v-icon>mdi-reload</v-icon>
</v-btn>
<v-btn icon class="ml-2">
<v-icon>mdi-file-pdf-box</v-icon>
</v-btn>
<v-btn icon class="ml-2">
<v-icon>mdi-file-excel-box</v-icon>
</v-btn>
</div>
</v-card-title>
<v-data-table :headers="headers" :items="filteredTasks" :search="search" :items-per-page="10" class="elevation-1" hide-default-footer>
<template v-slot:item="{ item }">
<tr @click="toggleExpandRow(item.id)">
<td>
<v-icon v-if="hasSubTasks(item.id)">
{{ isRowExpanded(item.id) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}
</v-icon>
</td>
<td>{{ item.subject }}</td>
<td>{{ item.assignee }}</td>
<td>{{ item.assigner }}</td>
<td>{{ getTaskTypeLabel(item.task_type_id) }}</td>
<td :class="getPriorityClass(item.priority)">{{ item.priority }}</td>
<td>{{ item.status }}</td>
<td>{{ item.start_date }}</td>
<td>{{ item.due_date }}</td>
<td>
<v-btn icon @click.stop="openEditModal(item)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon @click.stop="deleteTask(item.id)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</td>
</tr>
<tr v-if="isRowExpanded(item.id)">
<td :colspan="headers.length" class="related-task-row">
<v-data-table :headers="subHeaders" :items="getSubTasks(item.id)" class="elevation-0" hide-default-footer>
<template v-slot:item.priority="{ item }">
<span :class="getPriorityClass(item.priority)">{{ item.priority }}</span>
</template>
<template v-slot:item.actions="{ item }">
<v-btn icon @click.stop="openEditModal(item)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon @click.stop="deleteTask(item.id)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</template>
<template v-slot:item.task_type="{ item }">
{{ getTaskTypeLabel(item.task_type_id) }}
</template>
</v-data-table>
</td>
</tr>
</template>
</v-data-table>
<v-pagination v-model="page" :length="Math.ceil(filteredTasks.length / 10)" class="pa-4"></v-pagination>
</v-card>
</v-col>
</v-row>
<!-- Add Task Modal -->
<AddTaskModal v-if="showAddModal" @close="showAddModal = false" />
<!-- Edit Task Modal -->
<EditTaskModal v-if="showEditModal" @close="showEditModal = false" :task="selectedTask" />
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import AddTaskModal from './AddTaskModal.vue';
import EditTaskModal from './EditTaskModal.vue';
export default {
components: {
AddTaskModal,
EditTaskModal,
},
data() {
return {
bread: [
{ text: "Dashboard", disabled: false, to: "/dashboard" },
{ text: "Tasks", disabled: true, to: "" },
],
search: '',
page: 1,
showAddModal: false,
showEditModal: false,
selectedTask: null,
filter: 'all',
expandedRows: new Set(), // To track expanded rows
headers: [
{ text: 'Subject', value: 'subject', sortable: true },
{ text: 'Assignee', value: 'assignee', sortable: true },
{ text: 'Assigner', value: 'assigner', sortable: true },
{ text: 'Type', value: 'task_type', sortable: true },
{ text: 'Priority', value: 'priority', sortable: true },
{ text: 'Status', value: 'status', sortable: true },
{ text: 'Start Date', value: 'start_date', sortable: true },
{ text: 'Due Date', value: 'due_date', sortable: true },
{ text: '', value: 'actions', sortable: false },
],
subHeaders: [
{ text: 'Subject', value: 'subject', sortable: true },
{ text: 'Assignee', value: 'assignee', sortable: true },
{ text: 'Assigner', value: 'assigner', sortable: true },
{ text: 'Type', value: 'task_type', sortable: true },
{ text: 'Priority', value: 'priority', sortable: true },
{ text: 'Status', value: 'status', sortable: true },
{ text: 'Start Date', value: 'start_date', sortable: true },
{ text: 'Due Date', value: 'due_date', sortable: true },
{ text: '', value: 'actions', sortable: false },
],
};
},
computed: {
...mapState('tasks', {
tasks: state => state.tasks,
users: state => state.users,
priorities: state => state.priorities,
statuses: state => state.statuses,
taskTypes: state => state.taskTypes,
}),
...mapState('currentUser', {
user: state => state.user,
}),
filteredTasks() {
const tasksToFilter = this.filter === 'my' ? this.tasks.filter(task => task.assignee_id === this.user.id) : this.tasks;
return tasksToFilter.map(task => ({
...task,
assignee: this.getUserName(task.assignee_id),
assigner: this.getUserName(task.assigner_id),
priority: this.getPriorityLabel(task.priority_id),
status: this.getStatusLabel(task.status_id),
}));
},
},
methods: {
...mapActions('tasks', ['fetchTasks', 'deleteTask']),
reloadTasks() {
this.fetchTasks();
},
openAddModal() {
this.showAddModal = true;
},
openEditModal(task) {
this.selectedTask = task;
this.showEditModal = true;
},
deleteTask(item) {
this.$store.dispatch('tasks/deleteTask', item.id).then(() => {
this.reloadTasks();
});
},
getUserName(userId) {
const user = this.users.find(user => user.id === userId);
return user ? user.name : 'Unknown';
},
getPriorityLabel(priorityId) {
const priority = this.priorities.find(priority => priority.id === priorityId);
return priority ? priority.level : 'Unknown';
},
getStatusLabel(statusId) {
const status = this.statuses.find(status => status.id === statusId);
return status ? status.name : 'Unknown';
},
getTaskTypeLabel(id) {
const taskType = this.taskTypes.find(type => type.id === id);
return taskType ? taskType.name : 'Unknown';
},
getPriorityClass(priority) {
switch (priority) {
case 'Low':
return 'priority-low';
case 'Medium':
return 'priority-medium';
case 'High':
return 'priority-high';
default:
return '';
}
},
getSubTasks(taskId) {
return this.tasks.filter(task => task.previous_task_id === taskId).map(task => ({
...task,
assignee: this.getUserName(task.assignee_id),
assigner: this.getUserName(task.assigner_id),
priority: this.getPriorityLabel(task.priority_id),
status: this.getStatusLabel(task.status_id),
}));
},
hasSubTasks(taskId) {
return this.tasks.some(task => task.previous_task_id === taskId);
},
toggleExpandRow(taskId) {
if (this.expandedRows.has(taskId)) {
this.expandedRows.delete(taskId);
} else {
this.expandedRows.add(taskId);
}
},
isRowExpanded(taskId) {
return this.expandedRows.has(taskId);
},
},
watch: {
filter() {
this.page = 1; // Reset the pagination to the first page whenever the filter changes
},
},
mounted() {
this.reloadTasks();
// Fetch other necessary data
this.$store.dispatch('tasks/fetchUsers');
this.$store.dispatch('tasks/fetchPriorities');
this.$store.dispatch('tasks/fetchStatuses');
},
};
</script>
<style scoped>
.v-data-table {
border-bottom: none;
}
.v-data-table th {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.v-data-table td {
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.v-data-table .v-data-table__actions {
display: flex;
justify-content: flex-end;
}
.v-card-title .title {
font-size: 1.25rem;
}
.priority-low {
color: green;
}
.priority-medium {
color: orange;
}
.priority-high {
color: red;
}
/* Add a background color to expanded rows for better visibility */
.related-task-row .v-data-table__wrapper {
background-color: #f9f9f9;
border-left: 2px solid #e0e0e0;
/* Slight border to visually separate */
margin-top: 10px;
}
/* Indent related tasks */
.related-task-row {
margin-left: 20px;
}
/* Optional: Add some padding to related task rows for better spacing */
.related-task-row .v-data-table td {
padding-left: 16px;
}
</style>
Key Changes:
-
Expanded Rows Tracking: Added
expandedRowsas aSetto track which rows are expanded. -
Toggle Expand Row: Implemented
toggleExpandRowmethod to add/remove task IDs fromexpandedRows. -
Check Row Expansion: Implemented
isRowExpandedmethod to check if a row is expanded. - Subtasks Display: Adjusted the template to conditionally render subtasks when a row is expanded.
These changes should help you achieve the desired functionality of expandable rows in your table.