Adds in Mantis features. Enabling automated downloading of Mantises into the internal database, browsing of them, and viewing of attachments (including .msg files).

Resolves #14
This commit is contained in:
Cameron Redmore 2025-04-25 23:31:50 +01:00
parent 0e77e310bd
commit 5268d6aecd
15 changed files with 1583 additions and 44 deletions

249
src/pages/MantisPage.vue Normal file
View file

@ -0,0 +1,249 @@
<template>
<q-page padding>
<q-card
flat
bordered
class="q-mb-xl"
>
<q-card-section class="row items-center justify-between">
<div class="text-h4">
Mantis Tickets
</div>
<q-input
dense
debounce="300"
v-model="searchTerm"
placeholder="Search tickets..."
@update:model-value="fetchTickets(1)"
clearable
style="width: 300px"
>
<template #append>
<q-icon name="search" />
</template>
</q-input>
</q-card-section>
<q-table
:rows="tickets"
:columns="columns"
row-key="id"
v-model:pagination="pagination"
:loading="loading"
@request="handleTableRequest"
binary-state-sort
flat
bordered
@row-click="onRowClick"
class="cursor-pointer"
>
<template #body-cell-status="statusProps">
<q-td :props="statusProps">
<q-badge
:color="getStatusColor(statusProps.row.status)"
:label="statusProps.row.status"
/>
</q-td>
</template>
<template #body-cell-priority="priorityProps">
<q-td :props="priorityProps">
<q-badge
:color="getPriorityColor(priorityProps.row.priority)"
:label="priorityProps.row.priority"
/>
</q-td>
</template>
<template #body-cell-severity="severityProps">
<q-td :props="severityProps">
<q-badge
:color="getSeverityColor(severityProps.row.severity)"
:label="severityProps.row.severity"
/>
</q-td>
</template>
</q-table>
</q-card>
<mantis-ticket-dialog
v-model="showDialog"
:ticket-id="selectedTicketId"
@close="closeDialog"
/>
</q-page>
</template>
<script setup>
import { ref, onMounted, watch, defineProps } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
import { useQuasar } from 'quasar';
import MantisTicketDialog from 'src/components/MantisTicketDialog.vue';
const props = defineProps({
ticketId: {
type: [String, Number],
'default': null
}
});
const $q = useQuasar();
const tickets = ref([]);
const loading = ref(false);
const searchTerm = ref('');
const showDialog = ref(false);
const selectedTicketId = ref(null);
const router = useRouter();
const pagination = ref({
sortBy: 'updatedAt',
descending: true,
page: 1,
rowsPerPage: 25,
rowsNumber: 0 // Total number of rows/tickets
});
const columns = [
{ name: 'id', label: 'ID', field: 'id', align: 'left', sortable: true },
{ name: 'title', label: 'Title', field: 'title', align: 'left', sortable: true },
{ name: 'status', label: 'Status', field: 'status', align: 'center', sortable: true },
{ name: 'priority', label: 'Priority', field: 'priority', align: 'center', sortable: true },
{ name: 'severity', label: 'Severity', field: 'severity', align: 'center', sortable: true },
{ name: 'reporterUsername', label: 'Reporter', field: 'reporterUsername', align: 'left', sortable: true },
{ name: 'updatedAt', label: 'Last Updated', field: 'updatedAt', align: 'left', sortable: true, format: (val) => new Date(val).toLocaleString() },
];
const fetchTickets = async(page = pagination.value.page) =>
{
loading.value = true;
try
{
const params = {
page: page,
limit: pagination.value.rowsPerPage,
search: searchTerm.value || undefined,
// Add sorting params if needed based on pagination.sortBy and pagination.descending
};
const response = await axios.get('/api/mantis', { params });
tickets.value = response.data.data;
pagination.value.rowsNumber = response.data.pagination.total;
pagination.value.page = response.data.pagination.page; // Update current page
}
catch (error)
{
console.error('Error fetching Mantis tickets:', error);
$q.notify({
type: 'negative',
message: 'Failed to fetch Mantis tickets.'
});
}
finally
{
loading.value = false;
}
};
const handleTableRequest = (props) =>
{
const { page, rowsPerPage, sortBy, descending } = props.pagination;
pagination.value.page = page;
pagination.value.rowsPerPage = rowsPerPage;
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
fetchTickets(page);
};
const onRowClick = (evt, row) =>
{
//Change the route
router.push({ name: 'mantis', params: { ticketId: row.id } });
};
const openDialogForTicket = (id) =>
{
const ticketNum = Number(id);
if (!isNaN(ticketNum) && ticketNum > 0)
{
selectedTicketId.value = ticketNum;
showDialog.value = true;
}
};
const closeDialog = () =>
{
showDialog.value = false;
selectedTicketId.value = null;
//Reset the route to remove the ticketId from the URL
router.push({ name: 'mantis' });
};
// Watch for changes in the ticketId prop
watch(() => props.ticketId, (newTicketId) =>
{
if (newTicketId)
{
openDialogForTicket(newTicketId);
}
else
{
closeDialog();
}
});
onMounted(() =>
{
fetchTickets();
// Check initial prop value on mount
if (props.ticketId)
{
openDialogForTicket(props.ticketId);
}
});
// Helper functions for badge colors (customize as needed)
const getStatusColor = (status) =>
{
const lowerStatus = status?.toLowerCase();
if (lowerStatus === 'new') return 'blue';
if (lowerStatus === 'feedback') return 'orange';
if (lowerStatus === 'acknowledged') return 'purple';
if (lowerStatus === 'confirmed') return 'cyan';
if (lowerStatus === 'assigned') return 'teal';
if (lowerStatus === 'resolved') return 'green';
if (lowerStatus === 'closed') return 'grey';
return 'primary';
};
const getPriorityColor = (priority) =>
{
const lowerPriority = priority?.toLowerCase();
if (lowerPriority === 'none') return 'grey';
if (lowerPriority === 'low') return 'blue';
if (lowerPriority === 'normal') return 'green';
if (lowerPriority === 'high') return 'orange';
if (lowerPriority === 'urgent') return 'red';
if (lowerPriority === 'immediate') return 'purple';
return 'primary';
};
const getSeverityColor = (severity) =>
{
const lowerSeverity = severity?.toLowerCase();
if (lowerSeverity === 'feature') return 'info';
if (lowerSeverity === 'trivial') return 'grey';
if (lowerSeverity === 'text') return 'blue-grey';
if (lowerSeverity === 'tweak') return 'light-blue';
if (lowerSeverity === 'minor') return 'lime';
if (lowerSeverity === 'major') return 'amber';
if (lowerSeverity === 'crash') return 'deep-orange';
if (lowerSeverity === 'block') return 'red';
return 'primary';
};
</script>
<style scoped>
/* Add any specific styles here */
</style>