Skip to content

Commit 9b25b7f

Browse files
authored
feat(inspect): Media gallery improvements (#3203)
* remove item from view modal Signed-off-by: Colorado, Camilo <[email protected]> * download button Signed-off-by: Colorado, Camilo <[email protected]> * new end list hook Signed-off-by: Colorado, Camilo <[email protected]> * LoadMoreList component Signed-off-by: Colorado, Camilo <[email protected]> --------- Signed-off-by: Colorado, Camilo <[email protected]>
1 parent ea14818 commit 9b25b7f

23 files changed

+550
-72
lines changed

application/ui/src/components/virtualizer-grid-layout/virtualizer-grid-layout.component.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const VirtualizerGridLayout = <T extends { id?: string }>({
4848
gap: layoutOptions.minSpace?.height ?? MIN_SPACE,
4949
scrollToIndex,
5050
callback: (top) => {
51-
ref.current?.scrollTo({ top, behavior: 'smooth' });
51+
ref.current?.scrollTo?.({ top, behavior: 'smooth' });
5252
},
5353
});
5454

application/ui/src/features/inspect/dataset/dataset-list.component.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getThumbnailUrl } from '../utils';
1010
import { DatasetItemPlaceholder } from './dataset-item-placeholder/dataset-item-placeholder.component';
1111
import { getPlaceholderItem, isPlaceholderItem } from './dataset-item-placeholder/util';
1212
import { DeleteMediaItem } from './delete-dataset-item/delete-dataset-item.component';
13+
import { DownloadDatasetItem } from './download-dataset-item/download-dataset-item.component';
1314
import { useGetMediaItems } from './hooks/use-get-media-items.hook';
1415
import { MediaPreview } from './media-preview/media-preview.component';
1516
import { InferenceOpacityProvider } from './media-preview/providers/inference-opacity-provider.component';
@@ -77,6 +78,7 @@ export const DatasetList = () => {
7778
onDeleted={() => setSelectedMediaItem(null)}
7879
/>
7980
)}
81+
bottomLeftElement={() => <DownloadDatasetItem mediaItem={mediaItem} />}
8082
/>
8183
)
8284
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { ActionButton } from '@geti/ui';
5+
import { DownloadIcon } from '@geti/ui/icons';
6+
7+
import { MediaItem } from '../types';
8+
import { downloadFile } from './utils';
9+
10+
import classes from './download-dataset-item.module.scss';
11+
12+
export interface DownloadDatasetItemProps {
13+
mediaItem: MediaItem;
14+
}
15+
16+
export const DownloadDatasetItem = ({ mediaItem }: DownloadDatasetItemProps) => {
17+
const handleDownload = () => {
18+
const url = `/api/projects/${mediaItem.project_id}/images/${mediaItem.id}/full`;
19+
downloadFile(url, mediaItem.filename);
20+
};
21+
22+
return (
23+
<ActionButton
24+
isQuiet
25+
aria-label='download media item'
26+
UNSAFE_className={classes.downloadButton}
27+
onPress={handleDownload}
28+
>
29+
<DownloadIcon />
30+
</ActionButton>
31+
);
32+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
div:has(> .downloadButton) {
5+
padding: var(--spectrum-global-dimension-size-10);
6+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (C) 2025 Intel Corporation
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { ThemeProvider } from '@geti/ui/theme';
5+
import { fireEvent, render, screen } from '@testing-library/react';
6+
import { getMockedMediaItem } from 'mocks/mock-media-item';
7+
8+
import { MediaItem } from '../types';
9+
import { DownloadDatasetItem } from './download-dataset-item.component';
10+
import { downloadFile } from './utils';
11+
12+
vi.mock('./utils', async (importActual) => {
13+
const actual = await importActual<typeof import('./utils')>();
14+
return {
15+
...actual,
16+
downloadFile: vi.fn(),
17+
};
18+
});
19+
20+
describe('DownloadDatasetItem', () => {
21+
const renderComponent = (mediaItem: MediaItem) => {
22+
return render(
23+
<ThemeProvider>
24+
<DownloadDatasetItem mediaItem={mediaItem} />
25+
</ThemeProvider>
26+
);
27+
};
28+
29+
beforeEach(() => {
30+
vi.clearAllMocks();
31+
});
32+
33+
it('calls downloadFile with correct URL and filename when button is clicked', () => {
34+
const mockMediaItem = getMockedMediaItem({});
35+
36+
renderComponent(mockMediaItem);
37+
38+
const button = screen.getByLabelText('download media item');
39+
fireEvent.click(button);
40+
41+
expect(downloadFile).toHaveBeenCalledTimes(1);
42+
expect(downloadFile).toHaveBeenCalledWith(
43+
`/api/projects/${mockMediaItem.project_id}/images/${mockMediaItem.id}/full`,
44+
'test-image.jpg'
45+
);
46+
});
47+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const downloadFile = (url: string, name?: string) => {
2+
const link = document.createElement('a');
3+
4+
if (name) {
5+
link.download = name;
6+
}
7+
8+
link.href = url;
9+
link.hidden = true;
10+
link.click();
11+
12+
setTimeout(() => link.remove(), 100);
13+
};

application/ui/src/features/inspect/dataset/media-preview/media-preview.component.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
// Copyright (C) 2025 Intel Corporation
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { Button, ButtonGroup, Content, Dialog, dimensionValue, Divider, Grid, Header, Heading, View } from '@geti/ui';
4+
import {
5+
Button,
6+
ButtonGroup,
7+
Content,
8+
Dialog,
9+
dimensionValue,
10+
Divider,
11+
Grid,
12+
Header,
13+
Heading,
14+
Size,
15+
View,
16+
} from '@geti/ui';
517
import { isEmpty } from 'lodash-es';
618

719
import { MediaItem } from '../types';
@@ -17,6 +29,14 @@ type MediaPreviewProps = {
1729
onSelectedMediaItem: (mediaItem: string | null) => void;
1830
};
1931

32+
const layoutOptions = {
33+
maxColumns: 1,
34+
minSpace: new Size(8, 8),
35+
minItemSize: new Size(120, 120),
36+
maxItemSize: new Size(120, 120),
37+
preserveAspectRatio: true,
38+
};
39+
2040
export const MediaPreview = ({ mediaItems, selectedMediaItem, onClose, onSelectedMediaItem }: MediaPreviewProps) => {
2141
const { data: inferenceResult } = useMediaItemInference(selectedMediaItem);
2242

@@ -46,6 +66,7 @@ export const MediaPreview = ({ mediaItems, selectedMediaItem, onClose, onSelecte
4666
<View gridArea={'sidebar'}>
4767
<SidebarItems
4868
mediaItems={mediaItems}
69+
layoutOptions={layoutOptions}
4970
selectedMediaItem={selectedMediaItem}
5071
onSelectedMediaItem={onSelectedMediaItem}
5172
/>

application/ui/src/features/inspect/dataset/media-preview/sidebar-items/sidebar-items.component.tsx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
1-
import { Selection, Size, View } from '@geti/ui';
1+
import { Selection, View } from '@geti/ui';
2+
import { GridLayoutOptions } from 'react-aria-components';
23
import { getThumbnailUrl } from 'src/features/inspect/utils';
34

45
import { GridMediaItem } from '../../../../..//components/virtualizer-grid-layout/grid-media-item/grid-media-item.component';
56
import { MediaThumbnail } from '../../../../../components/media-thumbnail/media-thumbnail.component';
67
import { VirtualizerGridLayout } from '../../../../../components/virtualizer-grid-layout/virtualizer-grid-layout.component';
8+
import { DeleteMediaItem } from '../../delete-dataset-item/delete-dataset-item.component';
9+
import { DownloadDatasetItem } from '../../download-dataset-item/download-dataset-item.component';
710
import { MediaItem } from '../../types';
811

912
interface SidebarItemsProps {
1013
mediaItems: MediaItem[];
1114
selectedMediaItem: MediaItem;
15+
layoutOptions: GridLayoutOptions;
1216
onSelectedMediaItem: (mediaItem: string | null) => void;
1317
}
1418

15-
const layoutOptions = {
16-
maxColumns: 1,
17-
minSpace: new Size(8, 8),
18-
minItemSize: new Size(120, 120),
19-
maxItemSize: new Size(120, 120),
20-
preserveAspectRatio: true,
21-
};
22-
23-
export const SidebarItems = ({ mediaItems, selectedMediaItem, onSelectedMediaItem }: SidebarItemsProps) => {
19+
export const SidebarItems = ({
20+
mediaItems,
21+
layoutOptions,
22+
selectedMediaItem,
23+
onSelectedMediaItem,
24+
}: SidebarItemsProps) => {
2425
const selectedIndex = mediaItems.findIndex((item) => item.id === selectedMediaItem.id);
2526

2627
const handleSelectionChange = (newKeys: Selection) => {
@@ -31,6 +32,16 @@ export const SidebarItems = ({ mediaItems, selectedMediaItem, onSelectedMediaIte
3132
onSelectedMediaItem(mediaItem?.id ?? null);
3233
};
3334

35+
const handleDeletedItem = (deletedIds: string[]) => {
36+
if (deletedIds.includes(String(selectedMediaItem.id))) {
37+
const nextIndex = selectedIndex + 1;
38+
const newSelectedIndex = nextIndex < mediaItems.length - 1 ? nextIndex : selectedIndex - 1;
39+
const newSelectedItem = mediaItems[newSelectedIndex];
40+
41+
onSelectedMediaItem(newSelectedItem?.id ?? null);
42+
}
43+
};
44+
3445
return (
3546
<View width={'100%'} height={'100%'}>
3647
<VirtualizerGridLayout
@@ -41,15 +52,19 @@ export const SidebarItems = ({ mediaItems, selectedMediaItem, onSelectedMediaIte
4152
layoutOptions={layoutOptions}
4253
scrollToIndex={selectedIndex}
4354
onSelectionChange={handleSelectionChange}
44-
contentItem={(item) => (
55+
contentItem={(mediaItem) => (
4556
<GridMediaItem
4657
contentElement={() => (
4758
<MediaThumbnail
48-
alt={item.filename}
49-
url={getThumbnailUrl(item)}
50-
onClick={() => onSelectedMediaItem(item.id ?? null)}
59+
alt={mediaItem.filename}
60+
url={getThumbnailUrl(mediaItem)}
61+
onClick={() => onSelectedMediaItem(mediaItem.id ?? null)}
5162
/>
5263
)}
64+
topRightElement={() => (
65+
<DeleteMediaItem itemsIds={[String(mediaItem.id)]} onDeleted={handleDeletedItem} />
66+
)}
67+
bottomLeftElement={() => <DownloadDatasetItem mediaItem={mediaItem} />}
5368
/>
5469
)}
5570
/>

0 commit comments

Comments
 (0)