From d08c04cb211a808aec7e9172aabfbda1adb94c72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Weso=C5=82owski?= Date: Tue, 7 Jul 2026 13:19:40 +0200 Subject: [PATCH 1/2] fix: Submit/confirm prediction button disabled state --- .../secondary-toolbar.component.tsx | 23 +++- .../secondary-toolbar/util.test.ts | 108 ++++++++++++++++- .../media-preview/secondary-toolbar/util.ts | 24 ++++ .../annotation-actions-provider.test.tsx | 51 ++++++++ .../annotator/annotation-mappers.test.ts | 111 ++++++++++++++++++ .../shared/annotator/annotation-mappers.ts | 12 +- 6 files changed, 318 insertions(+), 11 deletions(-) create mode 100644 application/ui/src/shared/annotator/annotation-mappers.test.ts diff --git a/application/ui/src/features/dataset/media-preview/secondary-toolbar/secondary-toolbar.component.tsx b/application/ui/src/features/dataset/media-preview/secondary-toolbar/secondary-toolbar.component.tsx index e3b66c10643..018279cb8ef 100644 --- a/application/ui/src/features/dataset/media-preview/secondary-toolbar/secondary-toolbar.component.tsx +++ b/application/ui/src/features/dataset/media-preview/secondary-toolbar/secondary-toolbar.component.tsx @@ -22,7 +22,7 @@ import { AnnotatorModes } from './annotator-modes/annotator-modes-toggle.compone import { PredictionInferenceDevices } from './annotator-modes/prediction-inference-devices.component'; import { PredictionModelSelector } from './annotator-modes/prediction-model-selector.component'; import { PredictionButtons } from './annotator-modes/predictions-buttons.component'; -import { getNextItem } from './util'; +import { getIsSubmitDisabled, getNextItem } from './util'; import classes from './secondary-toolbar.module.scss'; @@ -95,8 +95,15 @@ export const SecondaryToolbar = ({ const { selectableModels } = usePredictionSetup(); const isPlaying = videoPlayerContext?.videoControls?.isPlaying ?? false; - const { canSubmit, isSaving, submitAnnotations, submitPredictions, initialAnnotations, initialPredictions } = - useAnnotationActions(); + const { + canSubmit, + hasInvalidAnnotation, + isSaving, + submitAnnotations, + submitPredictions, + initialAnnotations, + initialPredictions, + } = useAnnotationActions(); const handleSubmit = async () => { await submitAnnotations(subset); @@ -121,8 +128,14 @@ export const SecondaryToolbar = ({ const isPredictionMode = mode === 'prediction'; const isAnnotationMode = mode === 'annotation'; - // If annotations are not changed but subset has changed we want to allow user to submit - const isSubmitDisabled = (!canSubmit && !hasSubsetChanged) || isSaving || isLoadingPredictions; + const isSubmitDisabled = getIsSubmitDisabled({ + mode, + canSubmit, + hasInvalidAnnotation, + hasSubsetChanged, + isSaving, + isLoadingPredictions, + }); useHotkeys( HOTKEYS.submit, diff --git a/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.test.ts b/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.test.ts index 565e4a7b3dc..044e7917935 100644 --- a/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.test.ts +++ b/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.test.ts @@ -5,9 +5,115 @@ import { getMockedAnnotation } from 'mocks/mock-annotation'; import { getMockedAnnotationLabelRef, getMockedLabel } from 'mocks/mock-labels'; import { describe, expect, it } from 'vitest'; -import { toggleLabel } from './util'; +import { getIsSubmitDisabled, toggleLabel } from './util'; describe('secondary toolbar utils', () => { + describe('getIsSubmitDisabled', () => { + const defaultParams = { + mode: 'annotation' as const, + canSubmit: false, + hasInvalidAnnotation: false, + hasSubsetChanged: false, + isSaving: false, + isLoadingPredictions: false, + }; + + it('annotation mode: enabled when there is a new annotation', () => { + const result = getIsSubmitDisabled({ ...defaultParams, canSubmit: true, hasSubsetChanged: false }); + + expect(result).toBe(false); + }); + + it('annotation mode: enabled when annotations are unchanged but subset changed', () => { + const result = getIsSubmitDisabled({ ...defaultParams, canSubmit: false, hasSubsetChanged: true }); + + expect(result).toBe(false); + }); + + it('annotation mode: disabled when annotations and subset are both unchanged', () => { + const result = getIsSubmitDisabled({ ...defaultParams, canSubmit: false, hasSubsetChanged: false }); + + expect(result).toBe(true); + }); + + it('annotation mode: disabled when annotation is invalid, even if subset changed', () => { + const result = getIsSubmitDisabled({ + ...defaultParams, + canSubmit: false, + hasInvalidAnnotation: true, + hasSubsetChanged: true, + }); + + expect(result).toBe(true); + }); + + it('annotation mode: disabled while saving, regardless of other flags', () => { + const result = getIsSubmitDisabled({ + ...defaultParams, + canSubmit: true, + hasSubsetChanged: true, + isSaving: true, + }); + + expect(result).toBe(true); + }); + + it('prediction mode: enabled when a prediction is present and subset is unchanged', () => { + const result = getIsSubmitDisabled({ + ...defaultParams, + mode: 'prediction', + canSubmit: true, + hasSubsetChanged: false, + }); + + expect(result).toBe(false); + }); + + it('prediction mode: enabled when a prediction is present and subset changed', () => { + const result = getIsSubmitDisabled({ + ...defaultParams, + mode: 'prediction', + canSubmit: true, + hasSubsetChanged: true, + }); + + expect(result).toBe(false); + }); + + it('prediction mode: disabled when there is no prediction, even if subset changed', () => { + const result = getIsSubmitDisabled({ + ...defaultParams, + mode: 'prediction', + canSubmit: false, + hasSubsetChanged: true, + }); + + expect(result).toBe(true); + }); + + it('prediction mode: disabled when there is no prediction and subset is unchanged', () => { + const result = getIsSubmitDisabled({ + ...defaultParams, + mode: 'prediction', + canSubmit: false, + hasSubsetChanged: false, + }); + + expect(result).toBe(true); + }); + + it('prediction mode: disabled while loading predictions, regardless of other flags', () => { + const result = getIsSubmitDisabled({ + ...defaultParams, + mode: 'prediction', + canSubmit: true, + isLoadingPredictions: true, + }); + + expect(result).toBe(true); + }); + }); + describe('toggleLabel', () => { const mockLabel1 = getMockedLabel({ id: 'label-1', name: 'Label 1' }); const mockLabel2 = getMockedLabel({ id: 'label-2', name: 'Label 2' }); diff --git a/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.ts b/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.ts index 170d616acc4..e34e5d4b5ac 100644 --- a/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.ts +++ b/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.ts @@ -2,8 +2,32 @@ // SPDX-License-Identifier: Apache-2.0 import type { Label } from '../../../../constants/shared-types'; +import type { AnnotatorMode } from '../../../../shared/annotator/annotator-mode'; import type { AnnotationLabelRef } from '../../../../shared/types'; +type GetIsSubmitDisabledParams = { + mode: AnnotatorMode; + canSubmit: boolean; + hasInvalidAnnotation: boolean; + hasSubsetChanged: boolean; + isSaving: boolean; + isLoadingPredictions: boolean; +}; + +export const getIsSubmitDisabled = ({ + mode, + canSubmit, + hasInvalidAnnotation, + hasSubsetChanged, + isSaving, + isLoadingPredictions, +}: GetIsSubmitDisabledParams): boolean => { + const isContentSubmittable = + mode === 'prediction' ? canSubmit : !hasInvalidAnnotation && (canSubmit || hasSubsetChanged); + + return !isContentSubmittable || isSaving || isLoadingPredictions; +}; + export const toggleLabel = (newLabel: Label, labels: AnnotationLabelRef[]): AnnotationLabelRef[] => { const isExistingLabel = labels.some(({ id }) => id === newLabel.id); diff --git a/application/ui/src/shared/annotator/annotation-actions-provider.test.tsx b/application/ui/src/shared/annotator/annotation-actions-provider.test.tsx index 46f5c6330ac..e1d5d76de87 100644 --- a/application/ui/src/shared/annotator/annotation-actions-provider.test.tsx +++ b/application/ui/src/shared/annotator/annotation-actions-provider.test.tsx @@ -196,3 +196,54 @@ describe('Label normalization', () => { }); }); }); + +describe('canSubmit on initial load', () => { + it('is false when initial annotations are unchanged, including a null confidences field from the server', async () => { + const label1 = getMockedLabel({ id: 'label-1', name: 'Cat', color: '#FF0000' }); + + // This is the exact shape the backend returns for a human annotation with no confidence + // scores: `confidences` is present and `null` (see DatasetItemAnnotation), never omitted. + const annotationsDTO: AnnotationDTO[] = [ + { labels: [{ id: label1.id }], shape: getMockedShape({ type: 'rectangle' }), confidences: null }, + ]; + + const { result } = renderAnnotationActions({ + mode: 'annotation', + labels: [label1], + initialAnnotationsDTO: annotationsDTO, + }); + + await waitFor(() => expect(result.current).not.toBeNull()); + + await waitFor(() => { + expect(result.current.annotations).toHaveLength(1); + expect(result.current.canSubmit).toBe(false); + }); + }); + + it('is true once an unchanged annotation set is actually edited', async () => { + const label1 = getMockedLabel({ id: 'label-1', name: 'Cat', color: '#FF0000' }); + const label2 = getMockedLabel({ id: 'label-2', name: 'Dog', color: '#00FF00' }); + + const annotationsDTO: AnnotationDTO[] = [ + { labels: [{ id: label1.id }], shape: getMockedShape({ type: 'rectangle' }), confidences: null }, + ]; + + const { result } = renderAnnotationActions({ + mode: 'annotation', + labels: [label1, label2], + initialAnnotationsDTO: annotationsDTO, + }); + + await waitFor(() => expect(result.current.canSubmit).toBe(false)); + + act(() => { + result.current.addAnnotations([getMockedShape({ type: 'rectangle' })], [{ id: label2.id }]); + }); + + await waitFor(() => { + expect(result.current.annotations).toHaveLength(2); + expect(result.current.canSubmit).toBe(true); + }); + }); +}); diff --git a/application/ui/src/shared/annotator/annotation-mappers.test.ts b/application/ui/src/shared/annotator/annotation-mappers.test.ts new file mode 100644 index 00000000000..651bb2faa39 --- /dev/null +++ b/application/ui/src/shared/annotator/annotation-mappers.test.ts @@ -0,0 +1,111 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { getMockedShape } from 'mocks/mock-annotation'; +import { getMockedAnnotationLabelRef } from 'mocks/mock-labels'; + +import type { AnnotationDTO } from '../../constants/shared-types'; +import { mapLocalAnnotationsToServer, mapServerAnnotationsToLocal } from './annotation-mappers'; + +describe('mapServerAnnotationsToLocal', () => { + it('maps a label with no confidence score to a ref without a probability', () => { + const dto: AnnotationDTO[] = [ + { labels: [{ id: 'label-1' }], shape: getMockedShape({ type: 'rectangle' }), confidences: null }, + ]; + + const [annotation] = mapServerAnnotationsToLocal(dto); + + expect(annotation.labels).toEqual([{ id: 'label-1' }]); + expect(annotation.labels[0]).not.toHaveProperty('probability'); + }); + + it('maps a label with a confidence score to a ref with a matching probability', () => { + const dto: AnnotationDTO[] = [ + { labels: [{ id: 'label-1' }], shape: getMockedShape({ type: 'rectangle' }), confidences: [0.87] }, + ]; + + const [annotation] = mapServerAnnotationsToLocal(dto); + + expect(annotation.labels).toEqual([{ id: 'label-1', probability: 0.87 }]); + }); +}); + +describe('mapLocalAnnotationsToServer', () => { + it('maps confidences to null when no label has a probability', () => { + const annotation = { + id: 'annotation-1', + shape: getMockedShape({ type: 'rectangle' }), + labels: [getMockedAnnotationLabelRef({ id: 'label-1' })], + }; + + const [result] = mapLocalAnnotationsToServer([annotation]); + + expect(result).toHaveProperty('confidences', null); + }); + + it('maps confidences to an array of probabilities, in label order, when present', () => { + const annotation = { + id: 'annotation-1', + shape: getMockedShape({ type: 'rectangle' }), + labels: [ + getMockedAnnotationLabelRef({ id: 'label-1', probability: 0.6 }), + getMockedAnnotationLabelRef({ id: 'label-2', probability: 0.4 }), + ], + }; + + const [result] = mapLocalAnnotationsToServer([annotation]); + + expect(result.confidences).toEqual([0.6, 0.4]); + }); + + it('strips labels that are not in the provided valid id set', () => { + const annotation = { + id: 'annotation-1', + shape: getMockedShape({ type: 'rectangle' }), + labels: [ + getMockedAnnotationLabelRef({ id: 'label-1' }), + getMockedAnnotationLabelRef({ id: 'deleted-label' }), + ], + }; + + const [result] = mapLocalAnnotationsToServer([annotation], new Set(['label-1'])); + + expect(result.labels).toEqual([{ id: 'label-1' }]); + }); + + it('keeps all labels when no valid id set is provided', () => { + const annotation = { + id: 'annotation-1', + shape: getMockedShape({ type: 'rectangle' }), + labels: [getMockedAnnotationLabelRef({ id: 'label-1' })], + }; + + const [result] = mapLocalAnnotationsToServer([annotation]); + + expect(result.labels).toEqual([{ id: 'label-1' }]); + }); +}); + +describe('round trip: mapLocalAnnotationsToServer(mapServerAnnotationsToLocal(dto))', () => { + it('is deep-equal to a plain (non-prediction) server annotation, confidences included', () => { + // This is the exact shape the backend returns for a human annotation with no confidence + // scores: `confidences` is present and `null`, never omitted. + const dto: AnnotationDTO[] = [ + { labels: [{ id: 'label-1' }], shape: getMockedShape({ type: 'rectangle' }), confidences: null }, + ]; + + const roundTripped = mapLocalAnnotationsToServer(mapServerAnnotationsToLocal(dto)); + + expect(roundTripped).toEqual(dto); + }); + + it('is deep-equal to a prediction-style server annotation, confidences included', () => { + const dto: AnnotationDTO[] = [ + { labels: [{ id: 'label-1' }], shape: getMockedShape({ type: 'rectangle' }), confidences: [0.95] }, + ]; + + const roundTripped = mapLocalAnnotationsToServer(mapServerAnnotationsToLocal(dto)); + + expect(roundTripped).toEqual(dto); + }); +}); diff --git a/application/ui/src/shared/annotator/annotation-mappers.ts b/application/ui/src/shared/annotator/annotation-mappers.ts index 9cb2d3ab655..40cf1596c72 100644 --- a/application/ui/src/shared/annotator/annotation-mappers.ts +++ b/application/ui/src/shared/annotator/annotation-mappers.ts @@ -37,14 +37,16 @@ export const mapLocalAnnotationsToServer = ( const hasProbabilities = filteredLabels.some((ref) => ref.probability !== undefined); + const confidences = hasProbabilities + ? filteredLabels + .filter((labelRef): labelRef is Required => labelRef.probability !== undefined) + .map((labelRef) => labelRef.probability) + : null; + return { labels: filteredLabels.map(({ id }) => ({ id })), shape: annotation.shape, - ...(hasProbabilities && { - confidences: filteredLabels - .filter((labelRef): labelRef is Required => labelRef.probability !== undefined) - .map((labelRef) => labelRef.probability), - }), + confidences, }; }); }; From d2a6630e09a634be6a82d03b9c1c98a38827a831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20Weso=C5=82owski?= Date: Wed, 8 Jul 2026 15:30:40 +0200 Subject: [PATCH 2/2] refactor: replace getIsSubmitDisabled with useIsSubmitDisabled --- .../secondary-toolbar.component.tsx | 19 +-- .../use-is-submit-disabled.hook.test.tsx | 119 ++++++++++++++++++ .../use-is-submit-disabled.hook.ts | 24 ++++ .../secondary-toolbar/util.test.ts | 108 +--------------- .../media-preview/secondary-toolbar/util.ts | 24 ---- 5 files changed, 149 insertions(+), 145 deletions(-) create mode 100644 application/ui/src/features/dataset/media-preview/secondary-toolbar/use-is-submit-disabled.hook.test.tsx create mode 100644 application/ui/src/features/dataset/media-preview/secondary-toolbar/use-is-submit-disabled.hook.ts diff --git a/application/ui/src/features/dataset/media-preview/secondary-toolbar/secondary-toolbar.component.tsx b/application/ui/src/features/dataset/media-preview/secondary-toolbar/secondary-toolbar.component.tsx index 018279cb8ef..51d2bdc4f8c 100644 --- a/application/ui/src/features/dataset/media-preview/secondary-toolbar/secondary-toolbar.component.tsx +++ b/application/ui/src/features/dataset/media-preview/secondary-toolbar/secondary-toolbar.component.tsx @@ -22,7 +22,8 @@ import { AnnotatorModes } from './annotator-modes/annotator-modes-toggle.compone import { PredictionInferenceDevices } from './annotator-modes/prediction-inference-devices.component'; import { PredictionModelSelector } from './annotator-modes/prediction-model-selector.component'; import { PredictionButtons } from './annotator-modes/predictions-buttons.component'; -import { getIsSubmitDisabled, getNextItem } from './util'; +import { useIsSubmitDisabled } from './use-is-submit-disabled.hook'; +import { getNextItem } from './util'; import classes from './secondary-toolbar.module.scss'; @@ -95,15 +96,8 @@ export const SecondaryToolbar = ({ const { selectableModels } = usePredictionSetup(); const isPlaying = videoPlayerContext?.videoControls?.isPlaying ?? false; - const { - canSubmit, - hasInvalidAnnotation, - isSaving, - submitAnnotations, - submitPredictions, - initialAnnotations, - initialPredictions, - } = useAnnotationActions(); + const { isSaving, submitAnnotations, submitPredictions, initialAnnotations, initialPredictions } = + useAnnotationActions(); const handleSubmit = async () => { await submitAnnotations(subset); @@ -128,12 +122,9 @@ export const SecondaryToolbar = ({ const isPredictionMode = mode === 'prediction'; const isAnnotationMode = mode === 'annotation'; - const isSubmitDisabled = getIsSubmitDisabled({ + const isSubmitDisabled = useIsSubmitDisabled({ mode, - canSubmit, - hasInvalidAnnotation, hasSubsetChanged, - isSaving, isLoadingPredictions, }); diff --git a/application/ui/src/features/dataset/media-preview/secondary-toolbar/use-is-submit-disabled.hook.test.tsx b/application/ui/src/features/dataset/media-preview/secondary-toolbar/use-is-submit-disabled.hook.test.tsx new file mode 100644 index 00000000000..e5dfca322ba --- /dev/null +++ b/application/ui/src/features/dataset/media-preview/secondary-toolbar/use-is-submit-disabled.hook.test.tsx @@ -0,0 +1,119 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { type ReactNode } from 'react'; + +import { waitFor } from '@testing-library/react'; +import { getMockedShape } from 'mocks/mock-annotation'; +import { getMockedLabel } from 'mocks/mock-labels'; +import { getMockedMediaImage } from 'mocks/mock-media'; +import { getMockedProject } from 'mocks/mock-project'; +import { HttpResponse } from 'msw'; + +import { http } from '../../../../api/utils'; +import type { AnnotationDTO, Label } from '../../../../constants/shared-types'; +import { server } from '../../../../msw-node-setup'; +import { AnnotationActionsProvider } from '../../../../shared/annotator/annotation-actions-provider.component'; +import type { AnnotatorMode } from '../../../../shared/annotator/annotator-mode'; +import { renderHook } from '../../../../test-utils/render'; +import { useIsSubmitDisabled } from './use-is-submit-disabled.hook'; + +type RenderIsSubmitDisabledParams = { + mode?: AnnotatorMode; + hasSubsetChanged?: boolean; + isLoadingPredictions?: boolean; + initialAnnotationsDTO?: AnnotationDTO[]; + initialPredictionsDTO?: AnnotationDTO[]; + labels?: Label[]; +}; + +const renderIsSubmitDisabled = ({ + mode = 'annotation', + hasSubsetChanged = false, + isLoadingPredictions = false, + initialAnnotationsDTO = [], + initialPredictionsDTO = [], + labels = [], +}: RenderIsSubmitDisabledParams) => { + server.use( + http.get('/api/projects/{project_id}', () => + HttpResponse.json(getMockedProject({ task: { task_type: 'detection', exclusive_labels: false, labels } })) + ) + ); + + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + + return renderHook(() => useIsSubmitDisabled({ mode, hasSubsetChanged, isLoadingPredictions }), { wrapper }); +}; + +describe('useIsSubmitDisabled', () => { + const label1 = getMockedLabel({ id: 'label-1', name: 'Cat', color: '#FF0000' }); + + it('annotation mode: disabled when annotations and subset are both unchanged', async () => { + const annotationsDTO: AnnotationDTO[] = [ + { labels: [{ id: label1.id }], shape: getMockedShape({ type: 'rectangle' }), confidences: null }, + ]; + + const { result } = renderIsSubmitDisabled({ + mode: 'annotation', + labels: [label1], + initialAnnotationsDTO: annotationsDTO, + hasSubsetChanged: false, + }); + + await waitFor(() => expect(result.current).toBe(true)); + }); + + it('annotation mode: enabled when annotations are unchanged but subset changed', async () => { + const annotationsDTO: AnnotationDTO[] = [ + { labels: [{ id: label1.id }], shape: getMockedShape({ type: 'rectangle' }), confidences: null }, + ]; + + const { result } = renderIsSubmitDisabled({ + mode: 'annotation', + labels: [label1], + initialAnnotationsDTO: annotationsDTO, + hasSubsetChanged: true, + }); + + await waitFor(() => expect(result.current).toBe(false)); + }); + + it('prediction mode: enabled when a prediction is present', async () => { + const predictionsDTO: AnnotationDTO[] = [ + { labels: [{ id: label1.id }], shape: getMockedShape({ type: 'rectangle' }), confidences: [0.9] }, + ]; + + const { result } = renderIsSubmitDisabled({ + mode: 'prediction', + labels: [label1], + initialPredictionsDTO: predictionsDTO, + }); + + await waitFor(() => expect(result.current).toBe(false)); + }); + + it('prediction mode: disabled while loading predictions, even with a prediction present', async () => { + const predictionsDTO: AnnotationDTO[] = [ + { labels: [{ id: label1.id }], shape: getMockedShape({ type: 'rectangle' }), confidences: [0.9] }, + ]; + + const { result } = renderIsSubmitDisabled({ + mode: 'prediction', + labels: [label1], + initialPredictionsDTO: predictionsDTO, + isLoadingPredictions: true, + }); + + await waitFor(() => expect(result.current).toBe(true)); + }); +}); diff --git a/application/ui/src/features/dataset/media-preview/secondary-toolbar/use-is-submit-disabled.hook.ts b/application/ui/src/features/dataset/media-preview/secondary-toolbar/use-is-submit-disabled.hook.ts new file mode 100644 index 00000000000..37beed2f3a9 --- /dev/null +++ b/application/ui/src/features/dataset/media-preview/secondary-toolbar/use-is-submit-disabled.hook.ts @@ -0,0 +1,24 @@ +// Copyright (C) 2025-2026 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { useAnnotationActions } from '../../../../shared/annotator/annotation-actions-provider.component'; +import type { AnnotatorMode } from '../../../../shared/annotator/annotator-mode'; + +type UseIsSubmitDisabledParams = { + mode: AnnotatorMode; + hasSubsetChanged: boolean; + isLoadingPredictions: boolean; +}; + +export const useIsSubmitDisabled = ({ + mode, + hasSubsetChanged, + isLoadingPredictions, +}: UseIsSubmitDisabledParams): boolean => { + const { canSubmit, hasInvalidAnnotation, isSaving } = useAnnotationActions(); + + const isContentSubmittable = + mode === 'prediction' ? canSubmit : !hasInvalidAnnotation && (canSubmit || hasSubsetChanged); + + return !isContentSubmittable || isSaving || isLoadingPredictions; +}; diff --git a/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.test.ts b/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.test.ts index 044e7917935..565e4a7b3dc 100644 --- a/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.test.ts +++ b/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.test.ts @@ -5,115 +5,9 @@ import { getMockedAnnotation } from 'mocks/mock-annotation'; import { getMockedAnnotationLabelRef, getMockedLabel } from 'mocks/mock-labels'; import { describe, expect, it } from 'vitest'; -import { getIsSubmitDisabled, toggleLabel } from './util'; +import { toggleLabel } from './util'; describe('secondary toolbar utils', () => { - describe('getIsSubmitDisabled', () => { - const defaultParams = { - mode: 'annotation' as const, - canSubmit: false, - hasInvalidAnnotation: false, - hasSubsetChanged: false, - isSaving: false, - isLoadingPredictions: false, - }; - - it('annotation mode: enabled when there is a new annotation', () => { - const result = getIsSubmitDisabled({ ...defaultParams, canSubmit: true, hasSubsetChanged: false }); - - expect(result).toBe(false); - }); - - it('annotation mode: enabled when annotations are unchanged but subset changed', () => { - const result = getIsSubmitDisabled({ ...defaultParams, canSubmit: false, hasSubsetChanged: true }); - - expect(result).toBe(false); - }); - - it('annotation mode: disabled when annotations and subset are both unchanged', () => { - const result = getIsSubmitDisabled({ ...defaultParams, canSubmit: false, hasSubsetChanged: false }); - - expect(result).toBe(true); - }); - - it('annotation mode: disabled when annotation is invalid, even if subset changed', () => { - const result = getIsSubmitDisabled({ - ...defaultParams, - canSubmit: false, - hasInvalidAnnotation: true, - hasSubsetChanged: true, - }); - - expect(result).toBe(true); - }); - - it('annotation mode: disabled while saving, regardless of other flags', () => { - const result = getIsSubmitDisabled({ - ...defaultParams, - canSubmit: true, - hasSubsetChanged: true, - isSaving: true, - }); - - expect(result).toBe(true); - }); - - it('prediction mode: enabled when a prediction is present and subset is unchanged', () => { - const result = getIsSubmitDisabled({ - ...defaultParams, - mode: 'prediction', - canSubmit: true, - hasSubsetChanged: false, - }); - - expect(result).toBe(false); - }); - - it('prediction mode: enabled when a prediction is present and subset changed', () => { - const result = getIsSubmitDisabled({ - ...defaultParams, - mode: 'prediction', - canSubmit: true, - hasSubsetChanged: true, - }); - - expect(result).toBe(false); - }); - - it('prediction mode: disabled when there is no prediction, even if subset changed', () => { - const result = getIsSubmitDisabled({ - ...defaultParams, - mode: 'prediction', - canSubmit: false, - hasSubsetChanged: true, - }); - - expect(result).toBe(true); - }); - - it('prediction mode: disabled when there is no prediction and subset is unchanged', () => { - const result = getIsSubmitDisabled({ - ...defaultParams, - mode: 'prediction', - canSubmit: false, - hasSubsetChanged: false, - }); - - expect(result).toBe(true); - }); - - it('prediction mode: disabled while loading predictions, regardless of other flags', () => { - const result = getIsSubmitDisabled({ - ...defaultParams, - mode: 'prediction', - canSubmit: true, - isLoadingPredictions: true, - }); - - expect(result).toBe(true); - }); - }); - describe('toggleLabel', () => { const mockLabel1 = getMockedLabel({ id: 'label-1', name: 'Label 1' }); const mockLabel2 = getMockedLabel({ id: 'label-2', name: 'Label 2' }); diff --git a/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.ts b/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.ts index e34e5d4b5ac..170d616acc4 100644 --- a/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.ts +++ b/application/ui/src/features/dataset/media-preview/secondary-toolbar/util.ts @@ -2,32 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import type { Label } from '../../../../constants/shared-types'; -import type { AnnotatorMode } from '../../../../shared/annotator/annotator-mode'; import type { AnnotationLabelRef } from '../../../../shared/types'; -type GetIsSubmitDisabledParams = { - mode: AnnotatorMode; - canSubmit: boolean; - hasInvalidAnnotation: boolean; - hasSubsetChanged: boolean; - isSaving: boolean; - isLoadingPredictions: boolean; -}; - -export const getIsSubmitDisabled = ({ - mode, - canSubmit, - hasInvalidAnnotation, - hasSubsetChanged, - isSaving, - isLoadingPredictions, -}: GetIsSubmitDisabledParams): boolean => { - const isContentSubmittable = - mode === 'prediction' ? canSubmit : !hasInvalidAnnotation && (canSubmit || hasSubsetChanged); - - return !isContentSubmittable || isSaving || isLoadingPredictions; -}; - export const toggleLabel = (newLabel: Label, labels: AnnotationLabelRef[]): AnnotationLabelRef[] => { const isExistingLabel = labels.some(({ id }) => id === newLabel.id);