Compare commits
2 Commits
47697b4421
...
3d04995f97
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d04995f97 | |||
| d3069e1f25 |
@@ -262,6 +262,7 @@ export function TicketAssignmentsCard({
|
|||||||
{videoId ? (
|
{videoId ? (
|
||||||
<div className={'mt-4 border-t pt-4'}>
|
<div className={'mt-4 border-t pt-4'}>
|
||||||
<TicketDetectionPreview
|
<TicketDetectionPreview
|
||||||
|
key={`${videoId}:${selectedAssignment.id}`}
|
||||||
ticketId={ticketId}
|
ticketId={ticketId}
|
||||||
videoId={videoId}
|
videoId={videoId}
|
||||||
assignmentId={selectedAssignment.id}
|
assignmentId={selectedAssignment.id}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ interface TicketDetectionPreviewProps {
|
|||||||
onDetectionCountChange?: (count: number | undefined) => void;
|
onDetectionCountChange?: (count: number | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DETECTION_PAGE_SIZE = 10;
|
||||||
|
|
||||||
function DetectionMetadataBar({
|
function DetectionMetadataBar({
|
||||||
items,
|
items,
|
||||||
}: {
|
}: {
|
||||||
@@ -102,6 +104,8 @@ export function TicketDetectionPreview({
|
|||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
|
const [isDiscardDialogOpen, setIsDiscardDialogOpen] = useState(false);
|
||||||
const isUnassignedPreview = assignmentStatus === 'unassigned';
|
const isUnassignedPreview = assignmentStatus === 'unassigned';
|
||||||
|
const pageStart =
|
||||||
|
Math.floor(currentIndex / DETECTION_PAGE_SIZE) * DETECTION_PAGE_SIZE;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
@@ -109,23 +113,26 @@ export function TicketDetectionPreview({
|
|||||||
|
|
||||||
const queryParams = useMemo(
|
const queryParams = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
skip: currentIndex,
|
skip: pageStart,
|
||||||
limit: 1,
|
limit: DETECTION_PAGE_SIZE,
|
||||||
assignment_id: isUnassignedPreview ? undefined : assignmentId,
|
assignment_id: isUnassignedPreview ? undefined : assignmentId,
|
||||||
assignment_status: isUnassignedPreview
|
assignment_status: isUnassignedPreview
|
||||||
? ('unassigned' as const)
|
? ('unassigned' as const)
|
||||||
: undefined,
|
: undefined,
|
||||||
sort: 'timestamp_asc',
|
sort: 'timestamp_asc',
|
||||||
}),
|
}),
|
||||||
[assignmentId, currentIndex, isUnassignedPreview],
|
[assignmentId, isUnassignedPreview, pageStart],
|
||||||
);
|
);
|
||||||
|
|
||||||
const detectionsQuery = useTicketDetectionsQuery(
|
const detectionsQuery = useTicketDetectionsQuery(
|
||||||
videoId ?? undefined,
|
videoId ?? undefined,
|
||||||
queryParams,
|
queryParams,
|
||||||
|
{ keepPreviousPage: true },
|
||||||
);
|
);
|
||||||
const detectionResult = detectionsQuery.data;
|
const detectionResult = detectionsQuery.data;
|
||||||
const activeDetection = detectionResult?.items[0];
|
const activeDetection = detectionsQuery.isPlaceholderData
|
||||||
|
? undefined
|
||||||
|
: detectionResult?.items[currentIndex - pageStart];
|
||||||
const confirmedDetectionCount = detectionResult?.total;
|
const confirmedDetectionCount = detectionResult?.total;
|
||||||
const detectionCount = confirmedDetectionCount ?? 0;
|
const detectionCount = confirmedDetectionCount ?? 0;
|
||||||
const activeVisual = getDefectVisual(
|
const activeVisual = getDefectVisual(
|
||||||
@@ -156,12 +163,15 @@ export function TicketDetectionPreview({
|
|||||||
}, [confirmedDetectionCount, currentIndex]);
|
}, [confirmedDetectionCount, currentIndex]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDetectionCountChange?.(confirmedDetectionCount);
|
onDetectionCountChange?.(detectionCount);
|
||||||
}, [confirmedDetectionCount, onDetectionCountChange]);
|
}, [detectionCount, onDetectionCountChange]);
|
||||||
|
|
||||||
const isNavigationDisabled =
|
const isNavigationDisabled =
|
||||||
detectionCount === 0 || detectionsQuery.isLoading;
|
detectionCount === 0 ||
|
||||||
const isInitialLoading = detectionsQuery.isLoading;
|
detectionsQuery.isLoading ||
|
||||||
|
detectionsQuery.isPlaceholderData;
|
||||||
|
const isInitialLoading =
|
||||||
|
detectionsQuery.isLoading || detectionsQuery.isPlaceholderData;
|
||||||
const isError = detectionsQuery.isError;
|
const isError = detectionsQuery.isError;
|
||||||
const retry = () => void detectionsQuery.refetch();
|
const retry = () => void detectionsQuery.refetch();
|
||||||
|
|
||||||
@@ -221,7 +231,7 @@ export function TicketDetectionPreview({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isInitialLoading && !activeDetection ? (
|
{isInitialLoading ? (
|
||||||
<TicketDetectionPreviewSkeleton />
|
<TicketDetectionPreviewSkeleton />
|
||||||
) : isError ? (
|
) : isError ? (
|
||||||
<div className="rounded-lg border border-dashed border-destructive/40 bg-destructive/5 px-4 py-8 text-center">
|
<div className="rounded-lg border border-dashed border-destructive/40 bg-destructive/5 px-4 py-8 text-center">
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function TicketDetectionPreviewCard({
|
|||||||
<CardContent className="p-5">
|
<CardContent className="p-5">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<TicketDetectionPreview
|
<TicketDetectionPreview
|
||||||
|
key={`${videoId}:${assignmentId}`}
|
||||||
ticketId={ticketId}
|
ticketId={ticketId}
|
||||||
videoId={videoId}
|
videoId={videoId}
|
||||||
assignmentId={assignmentId}
|
assignmentId={assignmentId}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export function TicketUnassignedIssuesCard({
|
|||||||
<CardContent className={'p-5'}>
|
<CardContent className={'p-5'}>
|
||||||
<div className={'min-w-0'}>
|
<div className={'min-w-0'}>
|
||||||
<TicketDetectionPreview
|
<TicketDetectionPreview
|
||||||
|
key={`${videoId}:unassigned`}
|
||||||
ticketId={ticketId}
|
ticketId={ticketId}
|
||||||
videoId={videoId}
|
videoId={videoId}
|
||||||
assignmentStatus={'unassigned'}
|
assignmentStatus={'unassigned'}
|
||||||
|
|||||||
@@ -176,13 +176,9 @@ export function IssueReviewAction({
|
|||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="default"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onClick={() => void submitDecision('approve')}
|
onClick={() => void submitDecision('approve')}
|
||||||
className={cn(
|
|
||||||
decision === 'approve' &&
|
|
||||||
'border-emerald-500 bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{isPending && decision === 'approve' ? (
|
{isPending && decision === 'approve' ? (
|
||||||
<Loader2 className="animate-spin" />
|
<Loader2 className="animate-spin" />
|
||||||
@@ -193,13 +189,9 @@ export function IssueReviewAction({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="destructive"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onClick={() => void submitDecision('reject')}
|
onClick={() => void submitDecision('reject')}
|
||||||
className={cn(
|
|
||||||
decision === 'reject' &&
|
|
||||||
'border-destructive bg-destructive/10 text-destructive hover:bg-destructive/15',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{isPending && decision === 'reject' ? (
|
{isPending && decision === 'reject' ? (
|
||||||
<Loader2 className="animate-spin" />
|
<Loader2 className="animate-spin" />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
|
keepPreviousData,
|
||||||
useInfiniteQuery,
|
useInfiniteQuery,
|
||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
@@ -28,6 +29,11 @@ import { extensionRequestKeys } from '../../extension-requests/queries/extension
|
|||||||
|
|
||||||
const TICKET_TABLE_REFRESH_MS = 5 * 60 * 1000;
|
const TICKET_TABLE_REFRESH_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
interface TicketDetectionsQueryOptions {
|
||||||
|
enabled?: boolean;
|
||||||
|
keepPreviousPage?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface UseTicketsQueryParams {
|
interface UseTicketsQueryParams {
|
||||||
skip: number;
|
skip: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
@@ -113,8 +119,9 @@ export function useTicketAssignmentsQuery(
|
|||||||
export function useTicketDetectionsQuery(
|
export function useTicketDetectionsQuery(
|
||||||
videoId: string | undefined,
|
videoId: string | undefined,
|
||||||
params: VideoDetectionsParams,
|
params: VideoDetectionsParams,
|
||||||
enabled = true,
|
options: TicketDetectionsQueryOptions = {},
|
||||||
) {
|
) {
|
||||||
|
const { enabled = true, keepPreviousPage = false } = options;
|
||||||
const queryParams = useMemo(
|
const queryParams = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
skip: params.skip ?? 0,
|
skip: params.skip ?? 0,
|
||||||
@@ -143,6 +150,7 @@ export function useTicketDetectionsQuery(
|
|||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
videoService.getVideoDetections(videoId as string, queryParams),
|
videoService.getVideoDetections(videoId as string, queryParams),
|
||||||
enabled: Boolean(enabled && videoId),
|
enabled: Boolean(enabled && videoId),
|
||||||
|
placeholderData: keepPreviousPage ? keepPreviousData : undefined,
|
||||||
staleTime: Infinity,
|
staleTime: Infinity,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { getPinnedColumnStyles } from './columnStyles';
|
|||||||
|
|
||||||
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
||||||
return (
|
return (
|
||||||
<ShadTableHeader className="sticky top-0 z-20 bg-muted/70 backdrop-blur supports-backdrop-filter:bg-muted/60">
|
<ShadTableHeader className="sticky top-0 z-20 bg-muted">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id}>
|
<TableRow key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => {
|
{headerGroup.headers.map((header) => {
|
||||||
@@ -39,7 +39,7 @@ const TableHeader = <TData, _>({ table }: { table: Table<TData> }) => {
|
|||||||
canSort ? header.column.getToggleSortingHandler() : undefined
|
canSort ? header.column.getToggleSortingHandler() : undefined
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-11 max-w-0 overflow-hidden border-b border-border bg-muted/70 px-4 text-sm font-semibold text-foreground transition-colors',
|
'h-11 max-w-0 overflow-hidden border-b border-border bg-muted px-4 text-sm font-semibold text-foreground transition-colors',
|
||||||
canSort && 'cursor-pointer select-none hover:bg-muted',
|
canSort && 'cursor-pointer select-none hover:bg-muted',
|
||||||
sortDirection && 'bg-muted',
|
sortDirection && 'bg-muted',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,11 +41,7 @@ export default function AnnotatedDetectionImage({
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const aspectRatio =
|
const aspectRatio = aspectRatioOverride ?? '16 / 9';
|
||||||
aspectRatioOverride ??
|
|
||||||
(videoWidth > 0 && videoHeight > 0
|
|
||||||
? `${videoWidth} / ${videoHeight}`
|
|
||||||
: '16 / 9');
|
|
||||||
|
|
||||||
const annotatedMedia = objectUrl ? (
|
const annotatedMedia = objectUrl ? (
|
||||||
<>
|
<>
|
||||||
@@ -75,14 +71,7 @@ export default function AnnotatedDetectionImage({
|
|||||||
className="group relative flex w-full cursor-zoom-in items-center justify-center overflow-hidden rounded-lg border bg-black focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
className="group relative flex w-full cursor-zoom-in items-center justify-center overflow-hidden rounded-lg border bg-black focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||||
style={{ aspectRatio }}
|
style={{ aspectRatio }}
|
||||||
>
|
>
|
||||||
<Image
|
{annotatedMedia}
|
||||||
src={objectUrl}
|
|
||||||
alt={`${detection.detection.display_name} detection`}
|
|
||||||
fill
|
|
||||||
unoptimized
|
|
||||||
sizes="(min-width: 1280px) 18rem, (min-width: 640px) 50vw, 100vw"
|
|
||||||
className="object-contain"
|
|
||||||
/>
|
|
||||||
<span className="pointer-events-none absolute inset-x-0 bottom-0 z-[3] bg-gradient-to-t from-black/70 to-transparent px-3 pb-2 pt-8 text-center text-xs font-medium text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
<span className="pointer-events-none absolute inset-x-0 bottom-0 z-[3] bg-gradient-to-t from-black/70 to-transparent px-3 pb-2 pt-8 text-center text-xs font-medium text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
||||||
Click to view annotated image
|
Click to view annotated image
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user