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:
parent
0e77e310bd
commit
5268d6aecd
15 changed files with 1583 additions and 44 deletions
249
src/pages/MantisPage.vue
Normal file
249
src/pages/MantisPage.vue
Normal 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>
|
Loading…
Add table
Add a link
Reference in a new issue