Skip to content
Open
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,10 @@
import { useAnnotationCollections } from '$lib/hooks/useAnnotationCollections/useAnnotationCollections';
import { useAnnotationCollectionsFilter } from '$lib/hooks/useAnnotationCollectionsFilter/useAnnotationCollectionsFilter';
import { useSettings } from '$lib/hooks/useSettings';
import { usePostHog } from '$lib/hooks';
import { resolveEffectiveColorBySource } from '$lib/utils';
import { get } from 'svelte/store';
import { handleAnnotationSourceFilterChange } from './handleAnnotationSourceFilterChange';

interface Props {
collectionId: string;
Expand All @@ -20,6 +23,7 @@
const { setSelectedCollectionIds, selectedCollectionIds, seedSelectionIfNeeded } =
useAnnotationCollectionsFilter();
const { enforceColoringByClassStore } = useSettings();
const { trackEvent } = usePostHog();

const isEnabled = $derived(items.length > 1);

Expand All @@ -30,6 +34,17 @@
seedSelectionIfNeeded(collectionId, items);
}
});

const handleChangeSelectedItems = (newIds: string[]) => {
handleAnnotationSourceFilterChange({
newIds,
prevIds: get(selectedCollectionIds),
items,
collectionId,
setSelectedCollectionIds,
trackEvent
});
};
</script>

{#if isEnabled}
Expand All @@ -42,7 +57,7 @@
enableColorPicker
{items}
selectedItemsIds={$selectedCollectionIds}
onChangeSelectedItems={setSelectedCollectionIds}
onChangeSelectedItems={handleChangeSelectedItems}
/>
</Segment>
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@ import { type Writable } from 'svelte/store';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import AnnotationCollectionsMenu from './AnnotationCollectionsMenu.svelte';

const { trackEvent } = vi.hoisted(() => ({ trackEvent: vi.fn() }));
vi.mock('$lib/hooks', async (importOriginal) => {
const actual = await importOriginal();
return {
...(actual as object),
usePostHog: () => ({ trackEvent })
};
});

const mocks = vi.hoisted(() => ({
collections: [] as { collection_id: string; name: string }[],
selectedCollectionIds: null as unknown as Writable<string[]>,
Expand Down Expand Up @@ -48,6 +57,7 @@ describe('AnnotationCollectionsMenu', () => {
mocks.collections = [];
mocks.selectedCollectionIds.set([]);
mocks.enforceColoringByClassStore.set(false);
trackEvent.mockClear();
});

it('renders nothing when there are no collections', () => {
Expand Down Expand Up @@ -148,4 +158,42 @@ describe('AnnotationCollectionsMenu', () => {

expect(mocks.setSelectedCollectionIds).toHaveBeenLastCalledWith([]);
});

it('fires grid_filter_toggled with action unselected when a source is deselected', async () => {
mocks.collections = [
{ collection_id: 'c1', name: 'Dogs' },
{ collection_id: 'c2', name: 'Cats' }
];
mocks.selectedCollectionIds.set(['c1', 'c2']);
render(AnnotationCollectionsMenu, defaultProps);

await screen.getAllByRole('checkbox')[0].click();

expect(trackEvent).toHaveBeenCalledWith('grid_filter_toggled', {
collection_id: 'col-1',
filter_type: 'annotation_source',
filter_value: 'Dogs',
action: 'unselected',
active_count: 1
});
});

it('fires grid_filter_toggled with action selected when a source is selected', async () => {
mocks.collections = [
{ collection_id: 'c1', name: 'Dogs' },
{ collection_id: 'c2', name: 'Cats' }
];
mocks.selectedCollectionIds.set(['c2']);
render(AnnotationCollectionsMenu, defaultProps);

await screen.getAllByRole('checkbox')[0].click();

expect(trackEvent).toHaveBeenCalledWith('grid_filter_toggled', {
collection_id: 'col-1',
filter_type: 'annotation_source',
filter_value: 'Dogs',
action: 'selected',
active_count: 2
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, expect, it, vi } from 'vitest';
import { handleAnnotationSourceFilterChange } from './handleAnnotationSourceFilterChange';

const items = [
{ id: 'c1', name: 'Dogs' },
{ id: 'c2', name: 'Cats' }
];

describe('handleAnnotationSourceFilterChange', () => {
it('calls setSelectedCollectionIds with the new ids', () => {
const setSelectedCollectionIds = vi.fn();
const trackEvent = vi.fn();

handleAnnotationSourceFilterChange({
newIds: ['c2'],
prevIds: ['c1', 'c2'],
items,
collectionId: 'col-1',
setSelectedCollectionIds,
trackEvent
});

expect(setSelectedCollectionIds).toHaveBeenCalledWith(['c2']);
});

it('fires grid_filter_toggled with action unselected when a source is deselected', () => {
const setSelectedCollectionIds = vi.fn();
const trackEvent = vi.fn();

handleAnnotationSourceFilterChange({
newIds: ['c2'],
prevIds: ['c1', 'c2'],
items,
collectionId: 'col-1',
setSelectedCollectionIds,
trackEvent
});

expect(trackEvent).toHaveBeenCalledWith('grid_filter_toggled', {
collection_id: 'col-1',
filter_type: 'annotation_source',
filter_value: 'Dogs',
action: 'unselected',
active_count: 1
});
});

it('fires grid_filter_toggled with action selected when a source is selected', () => {
const setSelectedCollectionIds = vi.fn();
const trackEvent = vi.fn();

handleAnnotationSourceFilterChange({
newIds: ['c1', 'c2'],
prevIds: ['c2'],
items,
collectionId: 'col-1',
setSelectedCollectionIds,
trackEvent
});

expect(trackEvent).toHaveBeenCalledWith('grid_filter_toggled', {
collection_id: 'col-1',
filter_type: 'annotation_source',
filter_value: 'Dogs',
action: 'selected',
active_count: 2
});
});

it('does not fire trackEvent when the changed id is not found in items', () => {
const setSelectedCollectionIds = vi.fn();
const trackEvent = vi.fn();

handleAnnotationSourceFilterChange({
newIds: ['c2'],
prevIds: ['unknown', 'c2'],
items,
collectionId: 'col-1',
setSelectedCollectionIds,
trackEvent
});

expect(setSelectedCollectionIds).toHaveBeenCalledWith(['c2']);
expect(trackEvent).not.toHaveBeenCalled();
});

it('reports active_count as the length of newIds', () => {
const setSelectedCollectionIds = vi.fn();
const trackEvent = vi.fn();

handleAnnotationSourceFilterChange({
newIds: [],
prevIds: ['c1'],
items,
collectionId: 'col-1',
setSelectedCollectionIds,
trackEvent
});

expect(trackEvent).toHaveBeenCalledWith('grid_filter_toggled', {
collection_id: 'col-1',
filter_type: 'annotation_source',
filter_value: 'Dogs',
action: 'unselected',
active_count: 0
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
interface HandleAnnotationSourceFilterChangeParams {
/** The new set of selected collection IDs after the user's action. */
newIds: string[];
/** The set of selected collection IDs before the user's action. */
prevIds: string[];
/** All available annotation source items with their display names. */
items: { id: string; name: string }[];
/** The active dataset collection ID, used as context in the analytics event. */
collectionId: string;
/** Updates the persisted selection of annotation source IDs. */
setSelectedCollectionIds: (ids: string[]) => void;
/** Fires an analytics event. */
trackEvent: (eventName: string, properties?: Record<string, unknown>) => void;
}

export function handleAnnotationSourceFilterChange({
newIds,
prevIds,
items,
collectionId,
setSelectedCollectionIds,
trackEvent
}: HandleAnnotationSourceFilterChangeParams): void {
const prevSet = new Set(prevIds);
const nextSet = new Set(newIds);

const addedId = newIds.find((id) => !prevSet.has(id));
const removedId = prevIds.find((id) => !nextSet.has(id));

const action: 'selected' | 'unselected' = addedId ? 'selected' : 'unselected';
const changedId = addedId ?? removedId;
const changedItem = items.find((item) => item.id === changedId);

setSelectedCollectionIds(newIds);

if (changedItem) {
trackEvent('grid_filter_toggled', {
collection_id: collectionId,
filter_type: 'annotation_source',
filter_value: changedItem.name,
action,
active_count: newIds.length
});
}
}
1 change: 1 addition & 0 deletions lightly_studio_view/src/lib/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ export {
useImageAnnotationCounts,
useImageAnnotationCountsQueryKey
} from '$lib/hooks/useImageAnnotationCounts/useImageAnnotationCounts';
export { usePostHog } from '$lib/hooks/usePostHog';
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import { get, writable } from 'svelte/store';
import type { AnnotationLabel } from '$lib/services/types';

const { trackEvent } = vi.hoisted(() => ({ trackEvent: vi.fn() }));
vi.mock('$lib/hooks', () => ({
usePostHog: () => ({ trackEvent })
}));

const selectedAnnotationFilterIds = writable<Set<string>>(new Set());
const setSelectedAnnotationFilterIds = vi.fn((id: string) => {
selectedAnnotationFilterIds.update((state) => {
Expand Down Expand Up @@ -118,6 +123,7 @@ describe('useAnnotationsFilter', () => {
vi.clearAllMocks();
selectedAnnotationFilterIds.set(new Set());
annotationLabels = writable<AnnotationLabel[] | undefined>(mockLabels);
trackEvent.mockClear();
});

it('returns empty annotationFilterRows when no counts set', () => {
Expand Down Expand Up @@ -259,4 +265,54 @@ describe('useAnnotationsFilter', () => {

expect(get(selectedAnnotationFilterNames)).toEqual(['dog']);
});

it('fires grid_filter_toggled with action selected when label is not yet selected', () => {
const { toggleAnnotationFilterSelection } = useAnnotationsFilter({ annotationLabels });

toggleAnnotationFilterSelection('cat', 'col-1');

expect(trackEvent).toHaveBeenCalledWith('grid_filter_toggled', {
collection_id: 'col-1',
filter_type: 'annotation_label',
filter_value: 'cat',
action: 'selected',
active_count: 1
});
});

it('fires grid_filter_toggled with action unselected when label is already selected', () => {
selectedAnnotationFilterIds.set(new Set(['id-1']));
const { toggleAnnotationFilterSelection } = useAnnotationsFilter({ annotationLabels });

toggleAnnotationFilterSelection('cat', 'col-1');

expect(trackEvent).toHaveBeenCalledWith('grid_filter_toggled', {
collection_id: 'col-1',
filter_type: 'annotation_label',
filter_value: 'cat',
action: 'unselected',
active_count: 0
});
});

it('does not fire grid_filter_toggled for unknown label', () => {
const { toggleAnnotationFilterSelection } = useAnnotationsFilter({ annotationLabels });

toggleAnnotationFilterSelection('unknown', 'col-1');

expect(trackEvent).not.toHaveBeenCalled();
});

it('passes collectionId to grid_filter_toggled event', () => {
const { toggleAnnotationFilterSelection } = useAnnotationsFilter({ annotationLabels });

toggleAnnotationFilterSelection('cat', 'my-collection');

expect(trackEvent).toHaveBeenCalledWith(
'grid_filter_toggled',
expect.objectContaining({
collection_id: 'my-collection'
})
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Annotation } from '$lib/types';
import type { AnnotationLabel } from '$lib/services/types';
import { useGlobalStorage } from '../useGlobalStorage';
import { useTags } from '../useTags/useTags';
import { usePostHog } from '$lib/hooks';

/**
* Low-level hook: manages selected annotation label IDs and produces an AnnotationsFilter.
Expand Down Expand Up @@ -96,6 +97,8 @@ export function useAnnotationsFilter({
clearSelectedAnnotationFilterIds
} = useSelectedAnnotationsFilter(collectionId);

const { trackEvent } = usePostHog();

// Internal writable for annotation counts, set via setAnnotationCounts
const annotationCountsStore = writable<AnnotationCount[] | undefined>(undefined);

Expand Down Expand Up @@ -161,12 +164,25 @@ export function useAnnotationsFilter({
);

// Toggle by label name
const toggleAnnotationFilterSelection = (labelName: string) => {
const toggleAnnotationFilterSelection = (labelName: string, collectionId?: string) => {
const labelsMap = get(annotationFilterLabels);
const labelId = labelsMap[labelName];
if (labelId) {
toggleSelectedAnnotationFilterId(labelId);
}
if (!labelId) return;

const currentIds = get(selectedAnnotationFilterIds);
const action = currentIds.has(labelId) ? 'unselected' : 'selected';

toggleSelectedAnnotationFilterId(labelId);

const activeCount = get(selectedAnnotationFilterIds).size;

trackEvent('grid_filter_toggled', {
collection_id: collectionId,
Comment thread
LeonardoRosaa marked this conversation as resolved.
filter_type: 'annotation_label',
filter_value: labelName,
action,
active_count: activeCount
});
};

return {
Expand Down
Loading
Loading