Replace status tabs with multi-select checkbox dropdown, default to Open + In Progress
Status filtering now supports selecting multiple statuses via a dropdown with checkboxes. Backend updated to accept comma-separated status values using Prisma `in` operator. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { Trash2, Save } from 'lucide-react';
|
||||
import { Trash2, Save, Check } from 'lucide-react';
|
||||
import CTISelect from '../../components/CTISelect';
|
||||
import type { TicketStatus, User } from '../../types';
|
||||
import { TICKET_STATUSES } from '../../../../shared/schemas/enums';
|
||||
import type { User } from '../../types';
|
||||
import type { SavedView } from '../../../../shared/types';
|
||||
import { STATUS_LABELS } from '../../../../shared/constants/labels';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -13,16 +15,8 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
const STATUS_TABS: { value: TicketStatus | ''; label: string }[] = [
|
||||
{ value: '', label: 'All' },
|
||||
{ value: 'OPEN', label: 'Open' },
|
||||
{ value: 'IN_PROGRESS', label: 'In progress' },
|
||||
{ value: 'RESOLVED', label: 'Resolved' },
|
||||
{ value: 'CLOSED', label: 'Closed' },
|
||||
];
|
||||
|
||||
interface TicketFiltersProps {
|
||||
status: TicketStatus | '';
|
||||
status: string;
|
||||
severity: string;
|
||||
assigneeId: string;
|
||||
categoryId: string;
|
||||
@@ -64,25 +58,22 @@ export default function TicketFilters({
|
||||
total,
|
||||
isFetching,
|
||||
}: TicketFiltersProps) {
|
||||
const selectedStatuses = status ? status.split(',') : [];
|
||||
|
||||
const toggleStatus = (s: string) => {
|
||||
const next = selectedStatuses.includes(s)
|
||||
? selectedStatuses.filter((v) => v !== s)
|
||||
: [...selectedStatuses, s];
|
||||
onUpdateParam('status', next.length > 0 ? next.join(',') : null);
|
||||
};
|
||||
|
||||
const statusLabel =
|
||||
selectedStatuses.length === 0 || selectedStatuses.length === TICKET_STATUSES.length
|
||||
? 'All statuses'
|
||||
: selectedStatuses.map((s) => STATUS_LABELS[s] ?? s).join(', ');
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Status tabs */}
|
||||
<div className="flex items-center border-b border-border mb-4 -mx-4 px-4 overflow-x-auto">
|
||||
{STATUS_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => onUpdateParam('status', tab.value || null)}
|
||||
className={`px-3 py-2 text-sm whitespace-nowrap border-b-2 -mb-px transition-colors ${
|
||||
status === tab.value
|
||||
? 'border-primary text-foreground font-medium'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Row 1: Search + saved views + result count */}
|
||||
<div className="flex gap-2 mb-2 items-center">
|
||||
<input
|
||||
@@ -151,6 +142,31 @@ export default function TicketFilters({
|
||||
|
||||
{/* Row 2: Filter selectors */}
|
||||
<div className="flex gap-2 mb-4 items-center flex-wrap">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="gap-1.5 text-sm font-normal">
|
||||
{statusLabel}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-48">
|
||||
{TICKET_STATUSES.map((s) => (
|
||||
<DropdownMenuItem
|
||||
key={s}
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
toggleStatus(s);
|
||||
}}
|
||||
className="gap-2"
|
||||
>
|
||||
<div className="flex h-4 w-4 items-center justify-center rounded-sm border border-input">
|
||||
{selectedStatuses.includes(s) && <Check size={12} />}
|
||||
</div>
|
||||
{STATUS_LABELS[s] ?? s}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<select
|
||||
value={severity}
|
||||
onChange={(e) => onUpdateParam('severity', e.target.value || null)}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useShortcut } from '../../hooks/useShortcuts';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import Layout from '../../components/Layout';
|
||||
import { TicketStatus } from '../../types';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import {
|
||||
useTicketsPaged,
|
||||
@@ -44,7 +43,8 @@ export default function Tickets() {
|
||||
const { user: authUser } = useAuth();
|
||||
const { data: users = [] } = useUsers();
|
||||
|
||||
const status = (params.get('status') ?? '') as TicketStatus | '';
|
||||
const DEFAULT_STATUSES = 'OPEN,IN_PROGRESS';
|
||||
const status = params.get('status') ?? DEFAULT_STATUSES;
|
||||
const severity = params.get('severity') ?? '';
|
||||
const assigneeId = params.get('assigneeId') ?? '';
|
||||
const categoryId = params.get('categoryId') ?? '';
|
||||
@@ -88,7 +88,7 @@ export default function Tickets() {
|
||||
};
|
||||
|
||||
const queryParams = {
|
||||
status: status || undefined,
|
||||
status: status || undefined as string | undefined,
|
||||
severity: severity ? Number(severity) : undefined,
|
||||
assigneeId: assigneeId || undefined,
|
||||
categoryId: categoryId || undefined,
|
||||
|
||||
@@ -6,7 +6,14 @@ function whereConditions(query: string, filters: TicketFilters): Prisma.Sql[] {
|
||||
const conds: Prisma.Sql[] = [
|
||||
Prisma.sql`"searchVector" @@ plainto_tsquery('english', ${query})`,
|
||||
];
|
||||
if (filters.status) conds.push(Prisma.sql`"status" = ${filters.status}::"TicketStatus"`);
|
||||
if (filters.status) {
|
||||
const statuses = filters.status.split(',');
|
||||
if (statuses.length === 1) {
|
||||
conds.push(Prisma.sql`"status" = ${statuses[0]}::"TicketStatus"`);
|
||||
} else {
|
||||
conds.push(Prisma.sql`"status" IN (${Prisma.join(statuses.map(s => Prisma.sql`${s}::"TicketStatus"`))})`);
|
||||
}
|
||||
}
|
||||
if (filters.severity !== undefined) conds.push(Prisma.sql`"severity" = ${filters.severity}`);
|
||||
if (filters.assigneeId) conds.push(Prisma.sql`"assigneeId" = ${filters.assigneeId}`);
|
||||
if (filters.createdById) conds.push(Prisma.sql`"createdById" = ${filters.createdById}`);
|
||||
|
||||
@@ -80,7 +80,12 @@ export function findByIdOrDisplay(idOrDisplay: string) {
|
||||
|
||||
export function buildTicketWhere(filters: TicketFilters): Prisma.TicketWhereInput {
|
||||
const where: Prisma.TicketWhereInput = {};
|
||||
if (filters.status) where.status = filters.status as Prisma.TicketWhereInput['status'];
|
||||
if (filters.status) {
|
||||
const statuses = filters.status.split(',');
|
||||
where.status = statuses.length === 1
|
||||
? (statuses[0] as Prisma.TicketWhereInput['status'])
|
||||
: { in: statuses as string[] } as Prisma.TicketWhereInput['status'];
|
||||
}
|
||||
if (filters.severity) where.severity = filters.severity;
|
||||
if (filters.assigneeId) where.assigneeId = filters.assigneeId;
|
||||
if (filters.createdById) where.createdById = filters.createdById;
|
||||
|
||||
Reference in New Issue
Block a user