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