229 lines
6.6 KiB
TypeScript
229 lines
6.6 KiB
TypeScript
'use client';
|
|
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { X } from 'lucide-react';
|
|
|
|
import { MultiSelectPopover } from '@/components/form/MultiSelectPopover';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
import {
|
|
useDetectionCoordinatesQuery,
|
|
useTicketDetectionsQuery,
|
|
} from '../../../hooks/useTicketQueries';
|
|
import {
|
|
formatCompactRangeCoordinate,
|
|
formatRangeCoordinate,
|
|
type AssignmentRangePoint,
|
|
} from './assignmentRange';
|
|
import { AssignmentIssuesMap } from './AssignmentIssuesMap';
|
|
import { useIssueColumns } from './IssueColumns';
|
|
import { IssueTable } from './IssueTable';
|
|
|
|
const DEFAULT_PAGE_SIZE = 10;
|
|
const MAP_PAGE_SIZE = 50;
|
|
|
|
interface IssueTypeOption {
|
|
class_name: string;
|
|
display_name: string;
|
|
count: number;
|
|
}
|
|
|
|
interface ChooseIssuesSectionProps {
|
|
videoId?: string;
|
|
issueTypes: IssueTypeOption[];
|
|
selectedIssueTypes: string[];
|
|
startPoint: AssignmentRangePoint | null;
|
|
endPoint: AssignmentRangePoint | null;
|
|
onSelectedIssueTypesChange: (values: string[]) => void;
|
|
onStartPointChange: (point: AssignmentRangePoint | null) => void;
|
|
onEndPointChange: (point: AssignmentRangePoint | null) => void;
|
|
}
|
|
|
|
export function ChooseIssuesSection({
|
|
videoId,
|
|
issueTypes,
|
|
selectedIssueTypes,
|
|
startPoint,
|
|
endPoint,
|
|
onSelectedIssueTypesChange,
|
|
onStartPointChange,
|
|
onEndPointChange,
|
|
}: ChooseIssuesSectionProps) {
|
|
const [skip, setSkip] = useState(0);
|
|
const [limit, setLimit] = useState(DEFAULT_PAGE_SIZE);
|
|
const handleSetStart = useCallback(
|
|
(point: AssignmentRangePoint) => {
|
|
onStartPointChange(point);
|
|
onEndPointChange(null);
|
|
},
|
|
[onEndPointChange, onStartPointChange],
|
|
);
|
|
const handleSetEnd = useCallback(
|
|
(point: AssignmentRangePoint) => {
|
|
if (point.detectionId === startPoint?.detectionId) return;
|
|
|
|
onEndPointChange(point);
|
|
},
|
|
[onEndPointChange, startPoint?.detectionId],
|
|
);
|
|
const columns = useIssueColumns({
|
|
startPoint,
|
|
endPoint,
|
|
onSetStart: handleSetStart,
|
|
onSetEnd: handleSetEnd,
|
|
});
|
|
const issueTypeOptions = useMemo(
|
|
() =>
|
|
issueTypes.map((issue) => ({
|
|
value: issue.class_name,
|
|
label: issue.display_name,
|
|
})),
|
|
[issueTypes],
|
|
);
|
|
const handleIssueTypesChange = useCallback(
|
|
(values: string[]) => {
|
|
onSelectedIssueTypesChange(values);
|
|
onStartPointChange(null);
|
|
onEndPointChange(null);
|
|
},
|
|
[onEndPointChange, onSelectedIssueTypesChange, onStartPointChange],
|
|
);
|
|
useEffect(() => {
|
|
setSkip(0);
|
|
}, [selectedIssueTypes]);
|
|
|
|
const queryParams = useMemo(
|
|
() => ({
|
|
skip,
|
|
limit,
|
|
class_name:
|
|
selectedIssueTypes.length > 0 ? selectedIssueTypes : undefined,
|
|
sort: 'timestamp_asc',
|
|
}),
|
|
[limit, selectedIssueTypes, skip],
|
|
);
|
|
const detectionsQuery = useTicketDetectionsQuery(videoId, queryParams);
|
|
const mapQueryParams = useMemo(
|
|
() => ({
|
|
limit: MAP_PAGE_SIZE,
|
|
class_name:
|
|
selectedIssueTypes.length > 0 ? selectedIssueTypes : undefined,
|
|
assignment_status: 'unassigned' as const,
|
|
}),
|
|
[selectedIssueTypes],
|
|
);
|
|
const coordinatesQuery = useDetectionCoordinatesQuery(
|
|
videoId,
|
|
mapQueryParams,
|
|
);
|
|
const result = detectionsQuery.data;
|
|
const mapData = useMemo(() => {
|
|
const pages = coordinatesQuery.data?.pages;
|
|
|
|
if (!pages?.length) return undefined;
|
|
|
|
const latestPage = pages[pages.length - 1];
|
|
|
|
return {
|
|
...latestPage,
|
|
items: pages.flatMap((page) => page.items),
|
|
truncated: Boolean(coordinatesQuery.hasNextPage),
|
|
};
|
|
}, [coordinatesQuery.data?.pages, coordinatesQuery.hasNextPage]);
|
|
|
|
return (
|
|
<div className="min-w-0 space-y-5">
|
|
<Card size="sm" aria-label="Issue filters">
|
|
<CardHeader>
|
|
<CardTitle>Filters</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<MultiSelectPopover
|
|
label="Issue Types"
|
|
options={issueTypeOptions}
|
|
values={selectedIssueTypes}
|
|
onValuesChange={handleIssueTypesChange}
|
|
searchPlaceholder="Search issue types"
|
|
emptyMessage="No issue types found."
|
|
emptySelectionLabel="All"
|
|
align="start"
|
|
/>
|
|
|
|
{startPoint ? (
|
|
<Badge
|
|
variant="outline"
|
|
className="h-8 gap-1.5 px-2.5 font-normal"
|
|
title={formatRangeCoordinate(startPoint)}
|
|
>
|
|
<span className="font-medium">Start</span>
|
|
<span className="font-mono">
|
|
{formatCompactRangeCoordinate(startPoint)}
|
|
</span>
|
|
</Badge>
|
|
) : null}
|
|
|
|
{endPoint ? (
|
|
<Badge
|
|
variant="outline"
|
|
className="h-8 gap-1.5 px-2.5 font-normal"
|
|
title={formatRangeCoordinate(endPoint)}
|
|
>
|
|
<span className="font-medium">End</span>
|
|
<span className="font-mono">
|
|
{formatCompactRangeCoordinate(endPoint)}
|
|
</span>
|
|
</Badge>
|
|
) : null}
|
|
|
|
{startPoint !== null || endPoint !== null ? (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="size-8"
|
|
aria-label="Clear coordinate range"
|
|
onClick={() => {
|
|
onStartPointChange(null);
|
|
onEndPointChange(null);
|
|
}}
|
|
>
|
|
<X />
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<AssignmentIssuesMap
|
|
data={mapData}
|
|
isLoading={coordinatesQuery.isLoading}
|
|
isFetchingMore={coordinatesQuery.isFetchingNextPage}
|
|
isError={coordinatesQuery.isError}
|
|
hasMore={Boolean(coordinatesQuery.hasNextPage)}
|
|
startPoint={startPoint}
|
|
endPoint={endPoint}
|
|
onSetStart={handleSetStart}
|
|
onSetEnd={handleSetEnd}
|
|
onLoadMore={() => void coordinatesQuery.fetchNextPage()}
|
|
onRetry={() => void coordinatesQuery.refetch()}
|
|
/>
|
|
|
|
<IssueTable
|
|
columns={columns}
|
|
issues={result?.items ?? []}
|
|
isLoading={detectionsQuery.isLoading}
|
|
isError={detectionsQuery.isError}
|
|
toolbar={null}
|
|
skip={skip}
|
|
limit={limit}
|
|
total={result?.total ?? 0}
|
|
onPageChange={setSkip}
|
|
onLimitChange={setLimit}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|