Add filters.
This commit is contained in:
parent
a136b717bf
commit
8ad2c6ef53
4 changed files with 207 additions and 23 deletions
|
@ -0,0 +1,11 @@
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "MantisIssue_reporter_username_idx" ON "MantisIssue"("reporter_username");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "MantisIssue_status_idx" ON "MantisIssue"("status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "MantisIssue_priority_idx" ON "MantisIssue"("priority");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "MantisIssue_severity_idx" ON "MantisIssue"("severity");
|
|
@ -152,6 +152,11 @@ model MantisIssue {
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
comments MantisComment[]
|
comments MantisComment[]
|
||||||
|
|
||||||
|
@@index([reporterUsername])
|
||||||
|
@@index([status])
|
||||||
|
@@index([priority])
|
||||||
|
@@index([severity])
|
||||||
}
|
}
|
||||||
|
|
||||||
model MantisComment {
|
model MantisComment {
|
||||||
|
|
|
@ -10,10 +10,62 @@ const MsgReader = reader.default;
|
||||||
const prisma = new PrismaClient(); // Instantiate Prisma Client
|
const prisma = new PrismaClient(); // Instantiate Prisma Client
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Helper function to fetch distinct values
|
||||||
|
const getDistinctValues = async(field, res) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const values = await prisma.mantisIssue.findMany({
|
||||||
|
distinct: [field],
|
||||||
|
select: {
|
||||||
|
[field]: true,
|
||||||
|
},
|
||||||
|
where: { // Exclude null values if necessary
|
||||||
|
NOT: {
|
||||||
|
[field]: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
[field]: 'asc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
res.json(values.map(item => item[field]));
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error(`Error fetching distinct ${field} values:`, error.message);
|
||||||
|
res.status(500).json({ error: `Failed to fetch distinct ${field} values` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET /mantis/filters/statuses - Fetch unique status values
|
||||||
|
router.get('/filters/statuses', async(req, res) =>
|
||||||
|
{
|
||||||
|
await getDistinctValues('status', res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /mantis/filters/priorities - Fetch unique priority values
|
||||||
|
router.get('/filters/priorities', async(req, res) =>
|
||||||
|
{
|
||||||
|
await getDistinctValues('priority', res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /mantis/filters/severities - Fetch unique severity values
|
||||||
|
router.get('/filters/severities', async(req, res) =>
|
||||||
|
{
|
||||||
|
await getDistinctValues('severity', res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /mantis/filters/reporters - Fetch unique reporter usernames
|
||||||
|
router.get('/filters/reporters', async(req, res) =>
|
||||||
|
{
|
||||||
|
await getDistinctValues('reporterUsername', res);
|
||||||
|
});
|
||||||
|
|
||||||
// GET /mantis - Fetch multiple Mantis issues with filtering and pagination
|
// GET /mantis - Fetch multiple Mantis issues with filtering and pagination
|
||||||
router.get('/', async(req, res) =>
|
router.get('/', async(req, res) =>
|
||||||
{
|
{
|
||||||
const { page = 1, limit = 10, status, priority, severity, reporterUsername, search } = req.query;
|
const { page = 1, limit = 10, status, priority, severity, reporterUsername, search, sortBy = 'updatedAt', sortOrder = 'desc' } = req.query; // Add sortBy and sortOrder
|
||||||
|
|
||||||
const pageNum = parseInt(page, 10);
|
const pageNum = parseInt(page, 10);
|
||||||
const limitNum = parseInt(limit, 10);
|
const limitNum = parseInt(limit, 10);
|
||||||
|
@ -29,6 +81,7 @@ router.get('/', async(req, res) =>
|
||||||
where.OR = [
|
where.OR = [
|
||||||
{ title: { contains: search, mode: 'insensitive' } },
|
{ title: { contains: search, mode: 'insensitive' } },
|
||||||
{ description: { contains: search, mode: 'insensitive' } },
|
{ description: { contains: search, mode: 'insensitive' } },
|
||||||
|
{ comments: { some: { comment: { contains: search, mode: 'insensitive' } } } }, // Search in comments
|
||||||
];
|
];
|
||||||
|
|
||||||
// If the search term is a number, treat it as an ID
|
// If the search term is a number, treat it as an ID
|
||||||
|
@ -39,6 +92,16 @@ router.get('/', async(req, res) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate sortOrder
|
||||||
|
const validSortOrder = ['asc', 'desc'].includes(sortOrder) ? sortOrder : 'desc';
|
||||||
|
|
||||||
|
// Define allowed sort fields to prevent arbitrary sorting
|
||||||
|
const allowedSortFields = ['id', 'title', 'status', 'priority', 'severity', 'reporterUsername', 'createdAt', 'updatedAt'];
|
||||||
|
const validSortBy = allowedSortFields.includes(sortBy) ? sortBy : 'updatedAt';
|
||||||
|
|
||||||
|
const orderBy = {};
|
||||||
|
orderBy[validSortBy] = validSortOrder;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
let [issues, totalCount] = await prisma.$transaction([
|
let [issues, totalCount] = await prisma.$transaction([
|
||||||
|
@ -46,9 +109,7 @@ router.get('/', async(req, res) =>
|
||||||
where,
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limitNum,
|
take: limitNum,
|
||||||
orderBy: {
|
orderBy: orderBy, // Use dynamic orderBy
|
||||||
updatedAt: 'desc', // Default sort order
|
|
||||||
},
|
|
||||||
// You might want to include related data like comments count later
|
// You might want to include related data like comments count later
|
||||||
// include: { _count: { select: { comments: true } } }
|
// include: { _count: { select: { comments: true } } }
|
||||||
}),
|
}),
|
||||||
|
@ -83,9 +144,7 @@ router.get('/', async(req, res) =>
|
||||||
where,
|
where,
|
||||||
skip,
|
skip,
|
||||||
take: limitNum,
|
take: limitNum,
|
||||||
orderBy: {
|
orderBy: orderBy, // Use dynamic orderBy here as well
|
||||||
updatedAt: 'desc', // Default sort order
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (issues.length === 0)
|
if (issues.length === 0)
|
||||||
|
|
|
@ -5,23 +5,76 @@
|
||||||
bordered
|
bordered
|
||||||
class="q-mb-xl"
|
class="q-mb-xl"
|
||||||
>
|
>
|
||||||
<q-card-section class="row items-center justify-between">
|
<q-card-section class="row items-center justify-between q-gutter-md">
|
||||||
<div class="text-h4">
|
<div class="text-h4">
|
||||||
Mantis Tickets
|
Mantis Tickets
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row items-center q-gutter-sm">
|
||||||
|
<!-- Filters -->
|
||||||
|
<q-select
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
v-model="selectedStatus"
|
||||||
|
:options="statusOptions"
|
||||||
|
label="Status"
|
||||||
|
clearable
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
style="min-width: 120px"
|
||||||
|
@update:model-value="applyFilters"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
v-model="selectedPriority"
|
||||||
|
:options="priorityOptions"
|
||||||
|
label="Priority"
|
||||||
|
clearable
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
style="min-width: 120px"
|
||||||
|
@update:model-value="applyFilters"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
v-model="selectedSeverity"
|
||||||
|
:options="severityOptions"
|
||||||
|
label="Severity"
|
||||||
|
clearable
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
style="min-width: 120px"
|
||||||
|
@update:model-value="applyFilters"
|
||||||
|
/>
|
||||||
|
<q-select
|
||||||
|
dense
|
||||||
|
outlined
|
||||||
|
v-model="selectedReporter"
|
||||||
|
:options="reporterOptions"
|
||||||
|
label="Reporter"
|
||||||
|
clearable
|
||||||
|
emit-value
|
||||||
|
map-options
|
||||||
|
style="min-width: 150px"
|
||||||
|
@update:model-value="applyFilters"
|
||||||
|
/>
|
||||||
|
<!-- Search Input -->
|
||||||
<q-input
|
<q-input
|
||||||
dense
|
dense
|
||||||
|
outlined
|
||||||
debounce="300"
|
debounce="300"
|
||||||
v-model="searchTerm"
|
v-model="searchTerm"
|
||||||
placeholder="Search tickets..."
|
placeholder="Search tickets..."
|
||||||
@update:model-value="fetchTickets(1)"
|
@update:model-value="applyFilters"
|
||||||
clearable
|
clearable
|
||||||
style="width: 300px"
|
style="width: 250px"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<q-icon name="search" />
|
<q-icon name="search" />
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
</div>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
|
|
||||||
<q-table
|
<q-table
|
||||||
|
@ -123,7 +176,6 @@ import { useRouter } from 'vue-router';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useQuasar } from 'quasar';
|
import { useQuasar } from 'quasar';
|
||||||
import MantisTicketDialog from 'src/components/MantisTicketDialog.vue';
|
import MantisTicketDialog from 'src/components/MantisTicketDialog.vue';
|
||||||
|
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -143,6 +195,16 @@ const showSummaryDialog = ref(false); // New state for summary dialog
|
||||||
const summaryContent = ref(''); // New state for summary content
|
const summaryContent = ref(''); // New state for summary content
|
||||||
const summaryTicketId = ref(null); // New state for summary ticket ID
|
const summaryTicketId = ref(null); // New state for summary ticket ID
|
||||||
|
|
||||||
|
// Filter refs
|
||||||
|
const selectedStatus = ref(null);
|
||||||
|
const selectedPriority = ref(null);
|
||||||
|
const selectedSeverity = ref(null);
|
||||||
|
const selectedReporter = ref(null);
|
||||||
|
const statusOptions = ref([]);
|
||||||
|
const priorityOptions = ref([]);
|
||||||
|
const severityOptions = ref([]);
|
||||||
|
const reporterOptions = ref([]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
|
@ -164,6 +226,36 @@ const columns = [
|
||||||
{ name: 'actions', label: 'Actions', field: 'id', align: 'center' },
|
{ name: 'actions', label: 'Actions', field: 'id', align: 'center' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Fetch distinct filter values
|
||||||
|
const fetchFilterOptions = async() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const [statusRes, priorityRes, severityRes, reporterRes] = await Promise.all([
|
||||||
|
axios.get('/api/mantis/filters/statuses'),
|
||||||
|
axios.get('/api/mantis/filters/priorities'),
|
||||||
|
axios.get('/api/mantis/filters/severities'),
|
||||||
|
axios.get('/api/mantis/filters/reporters')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Format options for q-select
|
||||||
|
const formatOptions = (data) => data.map(value => ({ label: value, value }));
|
||||||
|
|
||||||
|
statusOptions.value = formatOptions(statusRes.data);
|
||||||
|
priorityOptions.value = formatOptions(priorityRes.data);
|
||||||
|
severityOptions.value = formatOptions(severityRes.data);
|
||||||
|
reporterOptions.value = formatOptions(reporterRes.data);
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Error fetching filter options:', error);
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
message: 'Failed to load filter options.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetchTickets = async(page = pagination.value.page) =>
|
const fetchTickets = async(page = pagination.value.page) =>
|
||||||
{
|
{
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
@ -173,8 +265,17 @@ const fetchTickets = async(page = pagination.value.page) =>
|
||||||
page: page,
|
page: page,
|
||||||
limit: pagination.value.rowsPerPage,
|
limit: pagination.value.rowsPerPage,
|
||||||
search: searchTerm.value || undefined,
|
search: searchTerm.value || undefined,
|
||||||
// Add sorting params if needed based on pagination.sortBy and pagination.descending
|
sortBy: pagination.value.sortBy, // Add sortBy
|
||||||
|
sortOrder: pagination.value.descending ? 'desc' : 'asc', // Add sortOrder
|
||||||
|
// Add filter parameters
|
||||||
|
status: selectedStatus.value || undefined,
|
||||||
|
priority: selectedPriority.value || undefined,
|
||||||
|
severity: selectedSeverity.value || undefined,
|
||||||
|
reporterUsername: selectedReporter.value || undefined,
|
||||||
};
|
};
|
||||||
|
// Remove undefined params
|
||||||
|
Object.keys(params).forEach(key => params[key] === undefined && delete params[key]);
|
||||||
|
|
||||||
const response = await axios.get('/api/mantis', { params });
|
const response = await axios.get('/api/mantis', { params });
|
||||||
tickets.value = response.data.data;
|
tickets.value = response.data.data;
|
||||||
pagination.value.rowsNumber = response.data.pagination.total;
|
pagination.value.rowsNumber = response.data.pagination.total;
|
||||||
|
@ -194,6 +295,13 @@ const fetchTickets = async(page = pagination.value.page) =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Function to apply filters and reset pagination
|
||||||
|
const applyFilters = () =>
|
||||||
|
{
|
||||||
|
pagination.value.page = 1; // Reset to first page when filters change
|
||||||
|
fetchTickets();
|
||||||
|
};
|
||||||
|
|
||||||
const handleTableRequest = (props) =>
|
const handleTableRequest = (props) =>
|
||||||
{
|
{
|
||||||
const { page, rowsPerPage, sortBy, descending } = props.pagination;
|
const { page, rowsPerPage, sortBy, descending } = props.pagination;
|
||||||
|
@ -244,6 +352,7 @@ watch(() => props.ticketId, (newTicketId) =>
|
||||||
|
|
||||||
onMounted(() =>
|
onMounted(() =>
|
||||||
{
|
{
|
||||||
|
fetchFilterOptions(); // Fetch filter options on mount
|
||||||
fetchTickets();
|
fetchTickets();
|
||||||
// Check initial prop value on mount
|
// Check initial prop value on mount
|
||||||
if (props.ticketId)
|
if (props.ticketId)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue