From 8ad2c6ef535d9ee65413b92a7f536cbfe21c389a Mon Sep 17 00:00:00 2001 From: Cameron Redmore Date: Sat, 26 Apr 2025 11:26:38 +0100 Subject: [PATCH] Add filters. --- .../migration.sql | 11 ++ prisma/schema.prisma | 5 + src-server/routes/mantis.js | 73 ++++++++- src/pages/MantisPage.vue | 141 ++++++++++++++++-- 4 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 prisma/migrations/20250426101758_add_mantis_indexes/migration.sql diff --git a/prisma/migrations/20250426101758_add_mantis_indexes/migration.sql b/prisma/migrations/20250426101758_add_mantis_indexes/migration.sql new file mode 100644 index 0000000..455b28e --- /dev/null +++ b/prisma/migrations/20250426101758_add_mantis_indexes/migration.sql @@ -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"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fbf861e..bbc3fa9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -152,6 +152,11 @@ model MantisIssue { updatedAt DateTime @updatedAt @map("updated_at") comments MantisComment[] + + @@index([reporterUsername]) + @@index([status]) + @@index([priority]) + @@index([severity]) } model MantisComment { diff --git a/src-server/routes/mantis.js b/src-server/routes/mantis.js index 04eafdf..811aceb 100644 --- a/src-server/routes/mantis.js +++ b/src-server/routes/mantis.js @@ -10,10 +10,62 @@ const MsgReader = reader.default; const prisma = new PrismaClient(); // Instantiate Prisma Client 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 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 limitNum = parseInt(limit, 10); @@ -29,6 +81,7 @@ router.get('/', async(req, res) => where.OR = [ { title: { 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 @@ -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 { let [issues, totalCount] = await prisma.$transaction([ @@ -46,9 +109,7 @@ router.get('/', async(req, res) => where, skip, take: limitNum, - orderBy: { - updatedAt: 'desc', // Default sort order - }, + orderBy: orderBy, // Use dynamic orderBy // You might want to include related data like comments count later // include: { _count: { select: { comments: true } } } }), @@ -83,9 +144,7 @@ router.get('/', async(req, res) => where, skip, take: limitNum, - orderBy: { - updatedAt: 'desc', // Default sort order - }, + orderBy: orderBy, // Use dynamic orderBy here as well }); if (issues.length === 0) diff --git a/src/pages/MantisPage.vue b/src/pages/MantisPage.vue index 43d715c..afcb439 100644 --- a/src/pages/MantisPage.vue +++ b/src/pages/MantisPage.vue @@ -5,23 +5,76 @@ bordered class="q-mb-xl" > - +
Mantis Tickets
- - - +
+ + + + + + + + + +
+{ + 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) => { loading.value = true; @@ -173,8 +265,17 @@ const fetchTickets = async(page = pagination.value.page) => page: page, limit: pagination.value.rowsPerPage, 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 }); tickets.value = response.data.data; 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 { page, rowsPerPage, sortBy, descending } = props.pagination; @@ -244,6 +352,7 @@ watch(() => props.ticketId, (newTicketId) => onMounted(() => { + fetchFilterOptions(); // Fetch filter options on mount fetchTickets(); // Check initial prop value on mount if (props.ticketId)