Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { createContext, ReactNode, useContext, useEffect, useRef, useState } from 'react';

import { useProjectIdentifier } from 'hooks/use-project-identifier.hook';
import { get } from 'lodash-es';
import { get, isEmpty, isObject } from 'lodash-es';
import { $api } from 'src/api/client';
import { components } from 'src/api/openapi-spec';
import { v4 as uuid } from 'uuid';
Expand Down Expand Up @@ -36,6 +36,7 @@ interface AnnotationsContextValue {
deleteAnnotations: (annotationIds: string[]) => void;
updateAnnotations: (updatedAnnotations: Annotation[]) => void;
submitAnnotations: () => Promise<void>;
isUserReviewed: boolean;
isSaving: boolean;
}

Expand All @@ -46,19 +47,30 @@ type AnnotationActionsProviderProps = {
mediaItem: DatasetItem;
};

const isUnannotatedError = (error: unknown): boolean => {
return (
isObject(error) && 'detail' in error && /Dataset item has not been annotated yet/i.test(String(error.detail))
);
};

export const AnnotationActionsProvider = ({ children, mediaItem }: AnnotationActionsProviderProps) => {
const projectId = useProjectIdentifier();
const saveMutation = $api.useMutation(
'post',
'/api/projects/{project_id}/dataset/items/{dataset_item_id}/annotations'
);
const { data: serverAnnotations } = $api.useQuery(

const { data: serverAnnotations, error: fetchError } = $api.useQuery(
'get',
'/api/projects/{project_id}/dataset/items/{dataset_item_id}/annotations',
{
params: { path: { project_id: projectId, dataset_item_id: mediaItem.id || '' } },
},
{
retry: (_failureCount, error: unknown) => !isUnannotatedError(error),
}
);

const { data: project } = $api.useQuery('get', '/api/projects/{project_id}', {
params: { path: { project_id: projectId } },
});
Expand Down Expand Up @@ -120,9 +132,16 @@ export const AnnotationActionsProvider = ({ children, mediaItem }: AnnotationAct
}
}, [serverAnnotations, project]);

useEffect(() => {
if (!isEmpty(fetchError)) {
setLocalAnnotations([]);
}
}, [fetchError]);

return (
<AnnotationsContext.Provider
value={{
isUserReviewed: get(serverAnnotations, 'user_reviewed', false),
annotations: localAnnotations,

// Local
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { MediaItem } from './media-item.component';
import { MediaThumbnail } from './media-thumbnail.component';
import { getThumbnailUrl } from './utils';

import classes from './gallery.module.scss';

type GalleryProps = {
items: DatasetItem[];
fetchNextPage: () => void;
Expand Down Expand Up @@ -52,6 +54,7 @@ export const Gallery = ({ items, hasNextPage, isFetchingNextPage, fetchNextPage
onSelectionChange={setSelectedKeys}
contentItem={(item) => (
<MediaItem
className={classes.mediaItem}
contentElement={() => (
<MediaThumbnail
alt={item.name}
Expand All @@ -77,7 +80,6 @@ export const Gallery = ({ items, hasNextPage, isFetchingNextPage, fetchNextPage
<DialogContainer onDismiss={() => setSelectedMediaItem(null)}>
{selectedMediaItem !== null && (
<MediaPreview
key={selectedMediaItem.id}
mediaItem={selectedMediaItem}
close={() => setSelectedMediaItem(null)}
onSelectedMediaItem={setSelectedMediaItem}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.mediaItem {
[data-floating-container='true'] {
background: var(--spectrum-global-color-gray-50);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { isFunction } from 'lodash-es';
import classes from './media-item.module.scss';

interface MediaItemProps {
className?: string;
contentElement: () => ReactNode;
topLeftElement?: () => ReactNode;
topRightElement?: () => ReactNode;
Expand All @@ -18,36 +19,49 @@ interface MediaItemProps {
}

export const MediaItem = ({
className,
contentElement,
topLeftElement,
topRightElement,
bottomLeftElement,
bottomRightElement,
}: MediaItemProps) => {
return (
<View width={'100%'}>
<View width={'100%'} UNSAFE_className={className}>
{contentElement()}

{isFunction(topLeftElement) && (
<View UNSAFE_className={clsx(classes.leftTopElement, classes.floatingContainer)}>
<View
data-floating-container
UNSAFE_className={clsx(classes.leftTopElement, classes.floatingContainer)}
>
{topLeftElement()}
</View>
)}

{isFunction(topRightElement) && (
<View UNSAFE_className={clsx(classes.rightTopElement, classes.floatingContainer)}>
<View
data-floating-container
UNSAFE_className={clsx(classes.rightTopElement, classes.floatingContainer)}
>
{topRightElement()}
</View>
)}

{isFunction(bottomLeftElement) && (
<View UNSAFE_className={clsx(classes.bottomLeftElement, classes.floatingContainer)}>
<View
data-floating-container
UNSAFE_className={clsx(classes.bottomLeftElement, classes.floatingContainer)}
>
{bottomLeftElement()}
</View>
)}

{isFunction(bottomRightElement) && (
<View UNSAFE_className={clsx(classes.bottomRightElement, classes.floatingContainer)}>
<View
data-floating-container
UNSAFE_className={clsx(classes.bottomRightElement, classes.floatingContainer)}
>
{bottomRightElement()}
</View>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
position: absolute;
line-height: 0px;
padding: var(--spectrum-global-dimension-size-125);
background: var(--spectrum-global-color-gray-50);
border-radius: var(--spectrum-global-dimension-size-50);

&:empty {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ZoomProvider } from '../../../components/zoom/zoom.provider';
import { AnnotatorCanvas } from '../../annotator/annotator-canvas';
import { SelectAnnotationProvider } from '../../annotator/select-annotation-provider.component';
import { DatasetItem } from '../../annotator/types';
import { AnnotatorButtons } from './annotator-buttons.component';
import { useGetDatasetItems } from '../gallery/use-get-dataset-items.hook';
import { ToolSelectionBar } from './primary-toolbar/primary-toolbar.component';
import { SecondaryToolbar } from './secondary-toolbar/secondary-toolbar.component';
import { SidebarItems } from './sidebar-items/sidebar-items.component';
Expand All @@ -30,6 +30,8 @@ const CanvasAreaLoading = () => (
);

export const MediaPreview = ({ mediaItem, close, onSelectedMediaItem }: MediaPreviewProps) => {
const { items, hasNextPage, isFetchingNextPage, fetchNextPage } = useGetDatasetItems();

return (
<Dialog UNSAFE_style={{ width: '95vw', height: '95vh' }}>
<Heading>Preview</Heading>
Expand Down Expand Up @@ -64,7 +66,12 @@ export const MediaPreview = ({ mediaItem, close, onSelectedMediaItem }: MediaPre
</View>

<View gridArea={'header'}>
<SecondaryToolbar />
<SecondaryToolbar
items={items}
onClose={close}
mediaItem={mediaItem}
onSelectedMediaItem={onSelectedMediaItem}
/>
</View>
<View gridArea={'canvas'} overflow={'hidden'}>
<AnnotatorCanvas mediaItem={mediaItem} />
Expand All @@ -75,11 +82,14 @@ export const MediaPreview = ({ mediaItem, close, onSelectedMediaItem }: MediaPre
</Suspense>

<View gridArea={'aside'}>
<SidebarItems mediaItem={mediaItem} onSelectedMediaItem={onSelectedMediaItem} />
</View>

<View gridArea={'footer'} padding={'size-100'} UNSAFE_style={{ textAlign: 'right' }}>
<AnnotatorButtons onClose={close} />
<SidebarItems
items={items}
mediaItem={mediaItem}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
onSelectedMediaItem={onSelectedMediaItem}
/>
</View>
</ZoomProvider>
</AnnotationActionsProvider>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,92 @@
// Copyright (C) 2025 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

import { Flex, Grid } from '@geti/ui';
import { Button, ButtonGroup, dimensionValue, Flex, Grid } from '@geti/ui';
import { QueryClient, useQueryClient } from '@tanstack/react-query';
import { isEmpty } from 'lodash-es';
import { useAnnotationActions } from 'src/features/annotator/annotation-actions-provider.component';
import { DatasetItem } from 'src/features/annotator/types';

import { DeleteMediaItem } from '../../gallery/delete-media-item/delete-media-item.component';
import { LabelPicker } from './label-picker.component';
import { useSecondaryToolbarState } from './use-secondary-toolbar-state.hook';

import classes from '../media-preview.module.scss';

export const SecondaryToolbar = () => {
type SecondaryToolbarProps = {
items: DatasetItem[];
mediaItem: DatasetItem;
onClose: () => void;
onSelectedMediaItem: (item: DatasetItem) => void;
};

const getNextItem = (totalItems: number, newIndex: number) => {
return Math.min(totalItems, newIndex + 1);
};

const invalidateMediaItemAnnotations = (queryClient: QueryClient) => {
queryClient.invalidateQueries({
queryKey: ['get', '/api/projects/{project_id}/dataset/items/{dataset_item_id}/annotations'],
});
};

export const SecondaryToolbar = ({ items, mediaItem, onClose, onSelectedMediaItem }: SecondaryToolbarProps) => {
const queryClient = useQueryClient();
const { annotations, isSaving, submitAnnotations } = useAnnotationActions();
const { isHidden, projectLabels, toggleLabels, annotationsToUpdate } = useSecondaryToolbarState();

const annotationLabelId = annotationsToUpdate.at(0)?.labels?.at(0)?.id;
const selectedLabel = projectLabels.find((label) => label.id === annotationLabelId) ?? null;
const hasAnnotations = !isEmpty(annotations);
const selectedIndex = items.findIndex((item) => item.id === mediaItem.id);

const handleSubmit = async () => {
await submitAnnotations();

const nextItem = getNextItem(items.length - 1, selectedIndex);
onSelectedMediaItem(items[nextItem]);

const isLastItem = selectedIndex === items.length - 1;
isLastItem && invalidateMediaItemAnnotations(queryClient);
};

const handleDeleteItem = ([deletedItem]: string[], totalItems: number) => {
const deletedIndex = items.findIndex((item) => item.id === deletedItem);
const nextItem = getNextItem(totalItems - 1, deletedIndex);

onSelectedMediaItem(items[nextItem]);
};

return (
<Flex height={'100%'} alignItems={'center'} margin={'size-100'} minHeight={'size-1200'}>
<Grid UNSAFE_className={classes.toolbarGrid} isHidden={isHidden}>
<Flex UNSAFE_className={classes.toolbarSection}>
<Flex
height={'100%'}
width={'100%'}
alignItems={'center'}
UNSAFE_style={{ paddingTop: dimensionValue('size-125') }}
>
<Grid width={'100%'} UNSAFE_className={classes.toolbarGrid} isHidden={isHidden}>
<Flex width={'100%'} UNSAFE_className={classes.toolbarSection} justifyContent={'space-between'}>
<LabelPicker selectedLabel={selectedLabel} labels={projectLabels} onSelect={toggleLabels} />

<ButtonGroup>
<DeleteMediaItem
itemsIds={[String(mediaItem.id)]}
onDeleted={([deletedItem]: string[]) => handleDeleteItem([deletedItem], items.length - 1)}
/>
<Button
variant='accent'
onPress={handleSubmit}
isPending={isSaving}
marginStart={'size-200'}
isDisabled={!hasAnnotations || isSaving}
>
Submit
</Button>

<Button variant='secondary' onPress={onClose} isDisabled={isSaving}>
Close
</Button>
</ButtonGroup>
</Flex>
</Grid>
</Flex>
Expand Down
Loading
Loading