feat(ticket): preserve list state across detail navigation
This commit is contained in:
@@ -11,7 +11,7 @@ const ModulesLayout = ({
|
|||||||
}>) => {
|
}>) => {
|
||||||
return (
|
return (
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<SidebarProvider>
|
<SidebarProvider defaultOpen={false}>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<main className="flex h-screen min-w-0 flex-1 flex-col overflow-hidden">
|
<main className="flex h-screen min-w-0 flex-1 flex-col overflow-hidden">
|
||||||
<div className="scroll-stable min-w-0 flex-1 overflow-auto">
|
<div className="scroll-stable min-w-0 flex-1 overflow-auto">
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ import { OpenRepairReviewAction } from './actions/OpenRepairReviewAction';
|
|||||||
|
|
||||||
export function TicketDetailHeader({
|
export function TicketDetailHeader({
|
||||||
ticket,
|
ticket,
|
||||||
|
backHref,
|
||||||
}: {
|
}: {
|
||||||
ticket: TicketOverviewDetail;
|
ticket: TicketOverviewDetail;
|
||||||
|
backHref: string;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const ticketName = ticket.ticket_name || 'Unnamed ticket';
|
const ticketName = ticket.ticket_name || 'Unnamed ticket';
|
||||||
@@ -27,7 +29,7 @@ export function TicketDetailHeader({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => router.push('/ticket')}
|
onClick={() => router.push(backHref)}
|
||||||
aria-label="Back to ticket list"
|
aria-label="Back to ticket list"
|
||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
import { ArrowLeft } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { PERMISSIONS } from '@/constants/permissions';
|
import { PERMISSIONS } from '@/constants/permissions';
|
||||||
import { PermissionGuard } from '@/guards';
|
import { PermissionGuard } from '@/guards';
|
||||||
|
import { ROUTES } from '@/utils/routes';
|
||||||
|
|
||||||
import { TicketDetailHeader } from './components/TicketDetailHeader';
|
import { TicketDetailHeader } from './components/TicketDetailHeader';
|
||||||
import {
|
import {
|
||||||
@@ -26,8 +27,20 @@ import {
|
|||||||
|
|
||||||
export default function TicketDetailPage() {
|
export default function TicketDetailPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const { ticketId } = useParams() as { ticketId: string };
|
const { ticketId } = useParams() as { ticketId: string };
|
||||||
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number>();
|
const [selectedAssignmentId, setSelectedAssignmentId] = useState<number>();
|
||||||
|
const listParams = new URLSearchParams();
|
||||||
|
|
||||||
|
for (const key of ['page', 'limit', 'segment']) {
|
||||||
|
const value = searchParams.get(key);
|
||||||
|
if (value) listParams.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listSearch = listParams.toString();
|
||||||
|
const ticketListHref = listSearch
|
||||||
|
? `${ROUTES.TICKET}?${listSearch}`
|
||||||
|
: ROUTES.TICKET;
|
||||||
|
|
||||||
const overviewQuery = useTicketOverviewQuery(ticketId);
|
const overviewQuery = useTicketOverviewQuery(ticketId);
|
||||||
const overview = overviewQuery.data;
|
const overview = overviewQuery.data;
|
||||||
@@ -52,7 +65,7 @@ export default function TicketDetailPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => router.push('/ticket')}
|
onClick={() => router.push(ticketListHref)}
|
||||||
>
|
>
|
||||||
<ArrowLeft />
|
<ArrowLeft />
|
||||||
Back to Tickets
|
Back to Tickets
|
||||||
@@ -65,7 +78,7 @@ export default function TicketDetailPage() {
|
|||||||
<main className="relative z-10 min-w-0 max-w-full space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden">
|
<main className="relative z-10 min-w-0 max-w-full space-y-5 xl:flex xl:h-[calc(100vh-6.5rem)] xl:min-h-0 xl:flex-col xl:gap-5 xl:space-y-0 xl:overflow-hidden">
|
||||||
<div className="xl:shrink-0">
|
<div className="xl:shrink-0">
|
||||||
{overview ? (
|
{overview ? (
|
||||||
<TicketDetailHeader ticket={overview} />
|
<TicketDetailHeader ticket={overview} backHref={ticketListHref} />
|
||||||
) : (
|
) : (
|
||||||
<TicketOverviewHeaderSkeleton />
|
<TicketOverviewHeaderSkeleton />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { PageHeader } from '@/components/page-header';
|
import { PageHeader } from '@/components/page-header';
|
||||||
import { Ticket } from 'lucide-react';
|
import { Ticket } from 'lucide-react';
|
||||||
import type { TicketListItem } from '@/types';
|
import type { TicketListItem } from '@/types';
|
||||||
@@ -17,11 +17,48 @@ import {
|
|||||||
useTicketSummaryQuery,
|
useTicketSummaryQuery,
|
||||||
} from './hooks/useTicketQueries';
|
} from './hooks/useTicketQueries';
|
||||||
|
|
||||||
|
const DEFAULT_PAGE = 1;
|
||||||
|
const DEFAULT_LIMIT = 10;
|
||||||
|
const PAGE_SIZE_OPTIONS = new Set([10, 20, 40, 50, 100]);
|
||||||
|
|
||||||
|
function parsePage(value: string | null) {
|
||||||
|
const page = Number(value);
|
||||||
|
return Number.isInteger(page) && page > 0 ? page : DEFAULT_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLimit(value: string | null) {
|
||||||
|
const limit = Number(value);
|
||||||
|
return PAGE_SIZE_OPTIONS.has(limit) ? limit : DEFAULT_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
export default function TicketPage() {
|
export default function TicketPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [skip, setSkip] = useState(0);
|
const pathname = usePathname();
|
||||||
const [limit, setLimit] = useState(10);
|
const searchParams = useSearchParams();
|
||||||
const [segmentId, setSegmentId] = useState('');
|
const searchParamsString = searchParams.toString();
|
||||||
|
const latestSearchParamsRef = useRef(searchParamsString);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
latestSearchParamsRef.current = searchParamsString;
|
||||||
|
}, [searchParamsString]);
|
||||||
|
|
||||||
|
const page = parsePage(searchParams.get('page'));
|
||||||
|
const limit = parseLimit(searchParams.get('limit'));
|
||||||
|
const segmentId = searchParams.get('segment') ?? '';
|
||||||
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const replaceListParams = useCallback(
|
||||||
|
(update: (params: URLSearchParams) => void) => {
|
||||||
|
const nextParams = new URLSearchParams(latestSearchParamsRef.current);
|
||||||
|
update(nextParams);
|
||||||
|
const nextSearch = nextParams.toString();
|
||||||
|
latestSearchParamsRef.current = nextSearch;
|
||||||
|
router.replace(nextSearch ? `${pathname}?${nextSearch}` : pathname, {
|
||||||
|
scroll: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[pathname, router],
|
||||||
|
);
|
||||||
|
|
||||||
useTenantTicketTableEvents();
|
useTenantTicketTableEvents();
|
||||||
|
|
||||||
@@ -36,16 +73,56 @@ export default function TicketPage() {
|
|||||||
const tickets = ticketsQuery.data?.items ?? [];
|
const tickets = ticketsQuery.data?.items ?? [];
|
||||||
const total = ticketsQuery.data?.total ?? 0;
|
const total = ticketsQuery.data?.total ?? 0;
|
||||||
|
|
||||||
const handleSegmentChange = useCallback((nextSegmentId: string) => {
|
const handleSegmentChange = useCallback(
|
||||||
setSegmentId(nextSegmentId);
|
(nextSegmentId: string) => {
|
||||||
setSkip(0);
|
replaceListParams((params) => {
|
||||||
}, []);
|
params.delete('page');
|
||||||
|
if (nextSegmentId) {
|
||||||
|
params.set('segment', nextSegmentId);
|
||||||
|
} else {
|
||||||
|
params.delete('segment');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[replaceListParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePageChange = useCallback(
|
||||||
|
(nextSkip: number) => {
|
||||||
|
const nextPage = Math.floor(nextSkip / limit) + 1;
|
||||||
|
replaceListParams((params) => {
|
||||||
|
if (nextPage === DEFAULT_PAGE) {
|
||||||
|
params.delete('page');
|
||||||
|
} else {
|
||||||
|
params.set('page', String(nextPage));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[limit, replaceListParams],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLimitChange = useCallback(
|
||||||
|
(nextLimit: number) => {
|
||||||
|
replaceListParams((params) => {
|
||||||
|
params.delete('page');
|
||||||
|
if (nextLimit === DEFAULT_LIMIT) {
|
||||||
|
params.delete('limit');
|
||||||
|
} else {
|
||||||
|
params.set('limit', String(nextLimit));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[replaceListParams],
|
||||||
|
);
|
||||||
|
|
||||||
const handleView = useCallback(
|
const handleView = useCallback(
|
||||||
(ticket: TicketListItem) => {
|
(ticket: TicketListItem) => {
|
||||||
router.push(ROUTES.TICKET_DETAIL(ticket.id));
|
const detailPath = ROUTES.TICKET_DETAIL(ticket.id);
|
||||||
|
router.push(
|
||||||
|
searchParamsString ? `${detailPath}?${searchParamsString}` : detailPath,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[router],
|
[router, searchParamsString],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toolbar = useMemo(
|
const toolbar = useMemo(
|
||||||
@@ -81,8 +158,8 @@ export default function TicketPage() {
|
|||||||
skip={skip}
|
skip={skip}
|
||||||
limit={limit}
|
limit={limit}
|
||||||
total={total}
|
total={total}
|
||||||
onPageChange={setSkip}
|
onPageChange={handlePageChange}
|
||||||
onLimitChange={setLimit}
|
onLimitChange={handleLimitChange}
|
||||||
onView={handleView}
|
onView={handleView}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user