From 95e8d7bba3efa619b8c49d8246ce7bf01cf111ef Mon Sep 17 00:00:00 2001 From: Leszek Date: Thu, 9 Jul 2026 13:37:43 +0200 Subject: [PATCH 01/15] already transcribed alert code --- .../BulkTranscriptionModal.tsx | 72 ++++++++- .../alerts/alertEvaluators.tests.ts | 142 ++++++++++++++++++ .../alerts/alertEvaluators.ts | 33 +++- .../BulkProcessingModals/alerts/types.ts | 2 + .../alerts/useBulkProcessingAlerts.ts | 1 + 5 files changed, 240 insertions(+), 10 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index 4a19b5501e..8d5755a4d6 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -1,6 +1,6 @@ import { Anchor, Group, Stack, Text } from '@mantine/core' import { useQueryClient } from '@tanstack/react-query' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { ACCOUNT_ROUTES } from '#/account/routes.constants' import type { ServerError } from '#/api/ServerError' @@ -18,16 +18,18 @@ import { import Alert from '#/components/common/alert' import RegionSelector from '#/components/languages/RegionSelector' import { getSuggestedLanguages } from '#/components/processing/common/utils' +import { getSupplementalPathParts } from '#/components/processing/processingUtils' import type { SubmissionResponse } from '#/dataInterface' import envStore from '#/envStore' import { useCalculateAudioDuration } from '#/hooks/useCalculateAudioDuration.hook' import { useSession } from '#/stores/useSession' -import { formatTimeFromSeconds, notify } from '#/utils' +import { convertSecondsToMinutes, formatTimeFromSeconds, notify } from '#/utils' import ButtonNew from '../../../common/ButtonNew' import LanguageSelector from '../../../languages/LanguageSelector' import type { LanguageCode } from '../../../languages/languagesStore' import { BulkProcessingWarningModal } from '../../BulkProcessingModals/BulkProcessingWarningModal' import BulkProcessingAlerts from '../alerts/BulkProcessingAlerts' +import { getAlertDefinitions } from '../alerts/alertDefinitions' import { useBulkProcessingAlerts } from '../alerts/useBulkProcessingAlerts' const GOOGLE_TRANSCRIPTION_LANGUAGE_SUPPORT_URL = 'transcription-translation.html#language-list' @@ -112,8 +114,23 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { activeBulkActions: props.activeBulkActions, }) + const { sourceRowPath } = getSupplementalPathParts(props.fieldXpath) + const eligibleSubmissionUuids = eligibleSubmissions.map((s) => s._uuid) + const alreadyTranscribedSubmissionUuids = useMemo( + () => activeAlerts.find((alert) => alert.id === 'already-transcribed')?.filteredSubmissionUuids ?? [], + [activeAlerts], + ) + + const alreadyTranscribedSubmissions = useMemo(() => { + if (alreadyTranscribedSubmissionUuids.length === 0) { + return [] + } + const uuids = new Set(alreadyTranscribedSubmissionUuids) + return props.selectedSubmissions.filter((submission) => uuids.has(submission._uuid)) + }, [alreadyTranscribedSubmissionUuids, props.selectedSubmissions]) + const { duration: audioDuration, isLoading: isAudioDurationLoading, @@ -123,10 +140,53 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { errorMessage: audioDurationErrorMesssage, } = useCalculateAudioDuration({ selectedSubmissions: eligibleSubmissions, - fieldId: props.fieldXpath, + fieldId: sourceRowPath, assetUid: props.assetUid, }) + const { + duration: alreadyTranscribedDuration, + isLoading: isAlreadyTranscribedDurationLoading, + isError: isAlreadyTranscribedDurationError, + } = useCalculateAudioDuration({ + selectedSubmissions: alreadyTranscribedSubmissions, + fieldId: sourceRowPath, + assetUid: props.assetUid, + }) + + const activeAlertsWithResolvedMinutes = useMemo(() => { + const alreadyTranscribedAlert = getAlertDefinitions('transcript').find( + (alert) => alert.id === 'already-transcribed', + ) + if (!alreadyTranscribedAlert) { + return activeAlerts + } + + const minutes = + isAlreadyTranscribedDurationLoading || isAlreadyTranscribedDurationError + ? '…' + : alreadyTranscribedDuration > 0 + ? Math.max(1, convertSecondsToMinutes(alreadyTranscribedDuration)) + : 0 + + return activeAlerts.map((alert) => { + if (alert.id !== 'already-transcribed') { + return alert + } + + const computedValues = { + ...alert.computedValues, + minutes, + } + + return { + ...alert, + computedValues, + message: alreadyTranscribedAlert.messageTemplate(computedValues), + } + }) + }, [activeAlerts, alreadyTranscribedDuration, isAlreadyTranscribedDurationError, isAlreadyTranscribedDurationLoading]) + const handleLanguageChange = (language: LanguageCode | null) => { setSelectedLanguage(language) setSelectedRegion(null) @@ -143,7 +203,7 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { uidAsset: props.assetUid, data: { action_id: ActionIdEnum.automatic_google_transcription, - question_xpath: props.fieldXpath, + question_xpath: sourceRowPath, submission_uuids: eligibleSubmissionUuids, params: { language: selectedLanguage!, @@ -212,10 +272,10 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { /> - + {/* Legacy alert - will be removed once evaluators are implemented */} - {hasExceededLimit && activeAlerts.length === 0 && ( + {hasExceededLimit && activeAlertsWithResolvedMinutes.length === 0 && ( {t("You've reached your automatic transcription limit. Please purchase an add‑on to continue.")} diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts index 5eee639f35..a6c7d9073f 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts @@ -2,6 +2,7 @@ import { expect } from 'chai' import assetDataFactory from '#/endpoints/assetData.factory' import { asrExceeded, mtExceeded, withinLimits } from '#/endpoints/serviceUsage.factory' import { + evaluateAlreadyTranscribed, evaluateAlreadyTranslated, evaluateNoEligibleSubmissions, evaluateNoSource, @@ -428,6 +429,147 @@ describe('evaluateAlreadyTranslated', () => { }) }) +describe('evaluateAlreadyTranscribed', () => { + const baseContext: AlertEvaluationContext = { + submissions: [], + fieldXpath: '_supplementalDetails/audio_question/transcript_en', + actionType: 'transcript', + activeBulkActions: [], + previouslyFilteredSubmissionUuids: new Set(), + } + + it('should not show alert when no submissions have transcripts', () => { + const mockSubmissions = [assetDataFactory(1, { _uuid: 'uuid-1' }), assetDataFactory(2, { _uuid: 'uuid-2' })] + + const context: AlertEvaluationContext = { + ...baseContext, + submissions: mockSubmissions, + } + + const result = evaluateAlreadyTranscribed(context) + + expect(result.shouldShow).to.equal(false) + expect(result.type).to.equal('warning') + expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result.computedValues).to.deep.equal({ + count: 0, + minutes: 0, + }) + }) + + it('should show alert when submissions have existing transcripts in any language', () => { + const mockSubmissions = [ + assetDataFactory(1, { + _uuid: 'uuid-1', + _supplementalDetails: { + audio_question: { + transcript: { + languageCode: 'fr', + value: 'Bonjour', + }, + }, + }, + }), + assetDataFactory(2, { + _uuid: 'uuid-2', + _supplementalDetails: { + audio_question: { + transcript: { + languageCode: 'sw', + value: 'Habari', + }, + }, + }, + }), + assetDataFactory(3, { _uuid: 'uuid-3' }), + ] + + const context: AlertEvaluationContext = { + ...baseContext, + submissions: mockSubmissions, + } + + const result = evaluateAlreadyTranscribed(context) + + expect(result.shouldShow).to.equal(true) + expect(result.type).to.equal('warning') + expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) + expect(result.computedValues).to.deep.equal({ + count: 2, + minutes: 0, + }) + }) + + it('should treat pending-review transcripts as existing', () => { + const mockSubmissions = [ + assetDataFactory(1, { + _uuid: 'uuid-1', + _supplementalDetails: { + audio_question: { + transcript: { + languageCode: 'en', + pendingReview: true, + }, + }, + }, + }), + ] + + const context: AlertEvaluationContext = { + ...baseContext, + submissions: mockSubmissions, + } + + const result = evaluateAlreadyTranscribed(context) + + expect(result.shouldShow).to.equal(true) + expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1']) + expect(result.computedValues.count).to.equal(1) + }) + + it('should skip submissions already filtered by previous evaluators', () => { + const mockSubmissions = [ + assetDataFactory(1, { + _uuid: 'uuid-1', + _supplementalDetails: { + audio_question: { + transcript: { + languageCode: 'en', + value: 'hello', + }, + }, + }, + }), + assetDataFactory(2, { + _uuid: 'uuid-2', + _supplementalDetails: { + audio_question: { + transcript: { + languageCode: 'es', + value: 'hola', + }, + }, + }, + }), + ] + + const context: AlertEvaluationContext = { + ...baseContext, + submissions: mockSubmissions, + previouslyFilteredSubmissionUuids: new Set(['uuid-1']), + } + + const result = evaluateAlreadyTranscribed(context) + + expect(result.shouldShow).to.equal(true) + expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2']) + expect(result.computedValues).to.deep.equal({ + count: 1, + minutes: 0, + }) + }) +}) + describe('evaluateNoSource', () => { describe('for transcription', () => { const baseContext: AlertEvaluationContext = { diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts index 560f72a276..22f6cc61d7 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts @@ -97,13 +97,38 @@ export function evaluateNoSource(context: AlertEvaluationContext): AlertEvaluati /** * Checks for submissions with existing transcripts - * TODO: DEV-1410 - Implement this evaluator (full duration calc depends on DEV-2255) */ export function evaluateAlreadyTranscribed(context: AlertEvaluationContext): AlertEvaluationResult { - console.log('[BulkProcessingAlerts] Evaluator evaluateAlreadyTranscribed - STUBBED, returning no alerts', context) + const { submissions, fieldXpath, previouslyFilteredSubmissionUuids } = context - // STUB: Return inactive result - return createInactiveResult('warning') + const { sourceRowPath } = getSupplementalPathParts(fieldXpath) + const alreadyTranscribed: string[] = [] + + submissions.forEach((submission) => { + // Skip if already filtered by previous evaluators + if (previouslyFilteredSubmissionUuids.has(submission._uuid)) { + return + } + + const transcript = submission._supplementalDetails?.[sourceRowPath]?.transcript + const hasTranscript = Boolean(transcript?.value || transcript?.pendingReview) + + if (hasTranscript) { + alreadyTranscribed.push(submission._uuid) + } + }) + + return { + shouldShow: alreadyTranscribed.length > 0, + type: 'warning', + filteredSubmissionUuids: alreadyTranscribed, + // The exact duration (in minutes) is resolved in the transcription modal + // with the audio-duration endpoint and replaces this placeholder value. + computedValues: { + count: alreadyTranscribed.length, + minutes: 0, + }, + } } /** diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts index c76b9110ba..ce8d5f2e55 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts @@ -62,4 +62,6 @@ export interface ActiveAlert { type: AlertSeverity message: string computedValues: Record + /** Optional UUIDs filtered by this alert, used by modal-specific follow-up calculations */ + filteredSubmissionUuids?: string[] } diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts index 942e4c6c66..0bf115e990 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts @@ -85,6 +85,7 @@ export function useBulkProcessingAlerts(props: UseBulkProcessingAlertsProps): Us type: alertDef.type, message, computedValues: result.computedValues, + filteredSubmissionUuids: result.filteredSubmissionUuids, }) } } From 9b9d314a8de675051330a9ee449aaf3097e6c4f0 Mon Sep 17 00:00:00 2001 From: Leszek Date: Thu, 9 Jul 2026 13:53:23 +0200 Subject: [PATCH 02/15] use seconds --- .../BulkTranscriptionModal/BulkTranscriptionModal.tsx | 10 ++++------ .../alerts/BulkProcessingAlerts.stories.tsx | 2 +- .../BulkProcessingModals/alerts/alertDefinitions.ts | 4 ++-- .../alerts/alertEvaluators.tests.ts | 6 +++--- .../BulkProcessingModals/alerts/alertEvaluators.ts | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index 8d5755a4d6..019b9b6c54 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -23,7 +23,7 @@ import type { SubmissionResponse } from '#/dataInterface' import envStore from '#/envStore' import { useCalculateAudioDuration } from '#/hooks/useCalculateAudioDuration.hook' import { useSession } from '#/stores/useSession' -import { convertSecondsToMinutes, formatTimeFromSeconds, notify } from '#/utils' +import { formatTimeFromSeconds, notify } from '#/utils' import ButtonNew from '../../../common/ButtonNew' import LanguageSelector from '../../../languages/LanguageSelector' import type { LanguageCode } from '../../../languages/languagesStore' @@ -162,12 +162,10 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { return activeAlerts } - const minutes = + const duration = isAlreadyTranscribedDurationLoading || isAlreadyTranscribedDurationError ? '…' - : alreadyTranscribedDuration > 0 - ? Math.max(1, convertSecondsToMinutes(alreadyTranscribedDuration)) - : 0 + : formatTimeFromSeconds(alreadyTranscribedDuration) return activeAlerts.map((alert) => { if (alert.id !== 'already-transcribed') { @@ -176,7 +174,7 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { const computedValues = { ...alert.computedValues, - minutes, + duration, } return { diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/BulkProcessingAlerts.stories.tsx b/jsapp/js/components/submissions/BulkProcessingModals/alerts/BulkProcessingAlerts.stories.tsx index 59a7ac4cbd..d1a1ceb353 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/BulkProcessingAlerts.stories.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/BulkProcessingAlerts.stories.tsx @@ -42,7 +42,7 @@ function createMockAlert( // Mock alerts const mockErrorAlert = createMockAlert('reached-limit', {}) const mockNearLimitAlert = createMockAlert('near-limit', { remainingMinutes: 5 }) -const mockWarningAlreadyTranscribed = createMockAlert('already-transcribed', { count: 3, minutes: 15 }) +const mockWarningAlreadyTranscribed = createMockAlert('already-transcribed', { count: 3, duration: '15 minutes' }) const mockWarningNoSource = createMockAlert('no-source', { count: 2 }) const mockWarningConflicting = createMockAlert('conflicting-job', {}) const mockErrorNoEligible = createMockAlert('no-eligible-submissions', { totalCount: 5, filteredCount: 5 }) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts index 60fa3ffff3..aa1f59f887 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts @@ -69,9 +69,9 @@ export function getAlertDefinitions(actionType: BulkActionType): AlertDefinition evaluator: isTranscription ? evaluateAlreadyTranscribed : evaluateAlreadyTranslated, messageTemplate: (values) => isTranscription - ? t('##count## audio files totaling ##minutes## minutes already transcribed and will be ignored') + ? t('##count## audio files totaling ##duration## already transcribed and will be ignored') .replace('##count##', String(values.count ?? 0)) - .replace('##minutes##', String(values.minutes ?? 0)) + .replace('##duration##', String(values.duration ?? 0)) : t('##count## transcripts totaling ##characters## characters already translated and will be ignored') .replace('##count##', String(values.count ?? 0)) .replace('##characters##', String(values.characters ?? 0)), diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts index a6c7d9073f..6c1522e6e4 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts @@ -453,7 +453,7 @@ describe('evaluateAlreadyTranscribed', () => { expect(result.filteredSubmissionUuids).to.deep.equal([]) expect(result.computedValues).to.deep.equal({ count: 0, - minutes: 0, + duration: 0, }) }) @@ -496,7 +496,7 @@ describe('evaluateAlreadyTranscribed', () => { expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) expect(result.computedValues).to.deep.equal({ count: 2, - minutes: 0, + duration: 0, }) }) @@ -565,7 +565,7 @@ describe('evaluateAlreadyTranscribed', () => { expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2']) expect(result.computedValues).to.deep.equal({ count: 1, - minutes: 0, + duration: 0, }) }) }) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts index 22f6cc61d7..16363d5b8d 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts @@ -126,7 +126,7 @@ export function evaluateAlreadyTranscribed(context: AlertEvaluationContext): Ale // with the audio-duration endpoint and replaces this placeholder value. computedValues: { count: alreadyTranscribed.length, - minutes: 0, + duration: 0, }, } } From c4e958cf9dcce652b0f7221a6359a57eba463807 Mon Sep 17 00:00:00 2001 From: Leszek Date: Thu, 9 Jul 2026 14:00:32 +0200 Subject: [PATCH 03/15] fallback to formatTimeFromSeconds --- .../BulkProcessingModals/alerts/alertDefinitions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts index aa1f59f887..8175cace2a 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts @@ -1,3 +1,4 @@ +import { formatTimeFromSeconds } from '#/utils' import { evaluateAlreadyTranscribed, evaluateAlreadyTranslated, @@ -71,10 +72,10 @@ export function getAlertDefinitions(actionType: BulkActionType): AlertDefinition isTranscription ? t('##count## audio files totaling ##duration## already transcribed and will be ignored') .replace('##count##', String(values.count ?? 0)) - .replace('##duration##', String(values.duration ?? 0)) + .replace('##duration##', String(values.duration ?? formatTimeFromSeconds(0))) : t('##count## transcripts totaling ##characters## characters already translated and will be ignored') .replace('##count##', String(values.count ?? 0)) - .replace('##characters##', String(values.characters ?? 0)), + .replace('##characters##', String(values.characters ?? formatTimeFromSeconds(0))), }, { id: 'no-eligible-submissions', From d0ed10cc14868b819c4f9ae1eafafbe5c658b099 Mon Sep 17 00:00:00 2001 From: Leszek Date: Thu, 9 Jul 2026 14:23:46 +0200 Subject: [PATCH 04/15] add comment --- .../BulkTranscriptionModal/BulkTranscriptionModal.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index 019b9b6c54..93d320bb83 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -154,6 +154,9 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { assetUid: props.assetUid, }) + // Keep useBulkProcessingAlerts generic, then enrich just the transcription-specific + // "already transcribed" warning with duration text calculated in this modal. + // All other alerts are rendered exactly as returned by the hook. const activeAlertsWithResolvedMinutes = useMemo(() => { const alreadyTranscribedAlert = getAlertDefinitions('transcript').find( (alert) => alert.id === 'already-transcribed', From b5600da408cb434ab31f5d35358f9e4335cc3caf Mon Sep 17 00:00:00 2001 From: Leszek Date: Thu, 9 Jul 2026 14:48:32 +0200 Subject: [PATCH 05/15] wip code --- .../BulkTranscriptionModal.tsx | 18 +++- .../BulkTranslationModal.tsx | 6 ++ .../alerts/alertDefinitions.ts | 2 +- .../alerts/alertEvaluators.tests.ts | 89 ++++++++++++++++++- .../alerts/alertEvaluators.ts | 42 ++++++++- .../BulkProcessingModals/alerts/types.ts | 2 + .../alerts/useBulkProcessingAlerts.ts | 5 ++ 7 files changed, 156 insertions(+), 8 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index 93d320bb83..1421b1050b 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -104,18 +104,32 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { const advancedFeatures = advancedFeaturesData?.status === 200 ? advancedFeaturesData.data : [] const suggestedLanguages = getSuggestedLanguages(advancedFeatures) + const { sourceRowPath } = getSupplementalPathParts(props.fieldXpath) + + const { + duration: totalSelectedAudioDuration, + isLoading: isTotalSelectedAudioDurationLoading, + isError: isTotalSelectedAudioDurationError, + } = useCalculateAudioDuration({ + selectedSubmissions: props.selectedSubmissions, + fieldId: sourceRowPath, + assetUid: props.assetUid, + }) + + const nearLimitRequiredSeconds = + isTotalSelectedAudioDurationLoading || isTotalSelectedAudioDurationError ? undefined : totalSelectedAudioDuration + const { activeAlerts, hasErrors, hasBlockingError, eligibleSubmissions } = useBulkProcessingAlerts({ actionType: 'transcript', selectedSubmissions: props.selectedSubmissions, selectedLanguage: selectedLanguage || undefined, selectedRegion: selectedRegion || undefined, fieldXpath: props.fieldXpath, + nearLimitRequiredAmount: nearLimitRequiredSeconds, serviceUsageData: serviceUsageData || undefined, activeBulkActions: props.activeBulkActions, }) - const { sourceRowPath } = getSupplementalPathParts(props.fieldXpath) - const eligibleSubmissionUuids = eligibleSubmissions.map((s) => s._uuid) const alreadyTranscribedSubmissionUuids = useMemo( diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx index 9fa49d74e3..37381832de 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx @@ -103,11 +103,17 @@ export function BulkTranslationModal(props: BulkTranslationModalProps) { const suggestedLanguages = getSuggestedLanguages(advancedFeatures) // Use bulk processing alerts hook + const nearLimitRequiredCharacters = props.selectedSubmissions.reduce((sum, sub) => { + const supplementalValue = getSupplementalDetailsContent(sub, props.fieldXpath) || '' + return sum + supplementalValue.length + }, 0) + const { activeAlerts, hasErrors, hasBlockingError, eligibleSubmissions } = useBulkProcessingAlerts({ actionType: 'translation', selectedSubmissions: props.selectedSubmissions, selectedLanguage: selectedLanguage || undefined, fieldXpath: props.fieldXpath, + nearLimitRequiredAmount: nearLimitRequiredCharacters, serviceUsageData: serviceUsageData || undefined, activeBulkActions: props.activeBulkActions, }) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts index 8175cace2a..15173da4cb 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts @@ -75,7 +75,7 @@ export function getAlertDefinitions(actionType: BulkActionType): AlertDefinition .replace('##duration##', String(values.duration ?? formatTimeFromSeconds(0))) : t('##count## transcripts totaling ##characters## characters already translated and will be ignored') .replace('##count##', String(values.count ?? 0)) - .replace('##characters##', String(values.characters ?? formatTimeFromSeconds(0))), + .replace('##characters##', String(values.characters ?? 0)), }, { id: 'no-eligible-submissions', diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts index 6c1522e6e4..539efaffef 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts @@ -1,9 +1,10 @@ import { expect } from 'chai' import assetDataFactory from '#/endpoints/assetData.factory' -import { asrExceeded, mtExceeded, withinLimits } from '#/endpoints/serviceUsage.factory' +import { asrExceeded, asrNearLimit, mtExceeded, mtNearLimit, withinLimits } from '#/endpoints/serviceUsage.factory' import { evaluateAlreadyTranscribed, evaluateAlreadyTranslated, + evaluateNearLimit, evaluateNoEligibleSubmissions, evaluateNoSource, evaluateReachedLimit, @@ -170,6 +171,92 @@ describe('evaluateReachedLimit', () => { }) }) +describe('evaluateNearLimit', () => { + const mockSubmissions = [assetDataFactory(1, { _uuid: 'uuid-1' }), assetDataFactory(2, { _uuid: 'uuid-2' })] + + const baseContext: AlertEvaluationContext = { + submissions: mockSubmissions, + fieldXpath: 'audio_question', + actionType: 'transcript', + activeBulkActions: [], + previouslyFilteredSubmissionUuids: new Set(), + } + + it('should show alert for transcription when remaining balance is positive but below required amount', () => { + const context: AlertEvaluationContext = { + ...baseContext, + serviceUsageData: asrNearLimit(95), + nearLimitRequiredAmount: 120, + } + + const result = evaluateNearLimit(context) + + expect(result.shouldShow).to.equal(true) + expect(result.type).to.equal('error') + expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result.computedValues).to.deep.equal({ + remainingMinutes: 0, + }) + }) + + it('should not show alert when remaining amount is enough to process the full job', () => { + const context: AlertEvaluationContext = { + ...baseContext, + serviceUsageData: asrNearLimit(95), + nearLimitRequiredAmount: 20, + } + + const result = evaluateNearLimit(context) + + expect(result.shouldShow).to.equal(false) + expect(result.type).to.equal('error') + }) + + it('should not show alert when balance is exceeded', () => { + const context: AlertEvaluationContext = { + ...baseContext, + serviceUsageData: asrExceeded(), + nearLimitRequiredAmount: 120, + } + + const result = evaluateNearLimit(context) + + expect(result.shouldShow).to.equal(false) + expect(result.type).to.equal('error') + }) + + it('should show alert for translation when remaining characters are below required amount', () => { + const context: AlertEvaluationContext = { + ...baseContext, + actionType: 'translation', + serviceUsageData: mtNearLimit(95), + nearLimitRequiredAmount: 3000, + } + + const result = evaluateNearLimit(context) + + expect(result.shouldShow).to.equal(true) + expect(result.type).to.equal('error') + expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result.computedValues).to.deep.equal({ + remainingCharacters: 2500, + }) + }) + + it('should not show alert when required amount is missing', () => { + const context: AlertEvaluationContext = { + ...baseContext, + serviceUsageData: asrNearLimit(95), + nearLimitRequiredAmount: undefined, + } + + const result = evaluateNearLimit(context) + + expect(result.shouldShow).to.equal(false) + expect(result.type).to.equal('error') + }) +}) + describe('evaluateAlreadyTranslated', () => { const baseContext: AlertEvaluationContext = { submissions: [], diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts index 16363d5b8d..b0f20c9aee 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts @@ -1,4 +1,5 @@ import { getSupplementalPathParts } from '#/components/processing/processingUtils' +import { convertSecondsToMinutes } from '#/utils' import type { AlertEvaluationContext, AlertEvaluationResult } from './types' import { createInactiveResult } from './utils' @@ -27,13 +28,46 @@ export function evaluateReachedLimit(context: AlertEvaluationContext): AlertEval /** * Checks if remaining quota is less than required but greater than 0 - * TODO: DEV-1399 - Implement this evaluator (depends on DEV-2255 for audio duration) */ export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluationResult { - console.log('[BulkProcessingAlerts] Evaluator evaluateNearLimit - STUBBED, returning no alerts', context) + const { actionType, serviceUsageData, nearLimitRequiredAmount } = context - // STUB: Return inactive result - return createInactiveResult('error') + if (!serviceUsageData?.balances || nearLimitRequiredAmount === undefined) { + return createInactiveResult('error') + } + + if (nearLimitRequiredAmount <= 0) { + return createInactiveResult('error') + } + + const balance = + actionType === 'transcript' ? serviceUsageData.balances.asr_seconds : serviceUsageData.balances.mt_characters + + if (!balance) { + return createInactiveResult('error') + } + + const remainingAmount = balance.balance_value + + if (remainingAmount <= 0 || remainingAmount >= nearLimitRequiredAmount) { + return createInactiveResult('error') + } + + const computedValues = + actionType === 'transcript' + ? { + remainingMinutes: convertSecondsToMinutes(remainingAmount), + } + : { + remainingCharacters: remainingAmount, + } + + return { + shouldShow: true, + type: 'error', + filteredSubmissionUuids: [], + computedValues, + } } /** diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts index ce8d5f2e55..63724ef704 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts @@ -23,6 +23,8 @@ export interface AlertEvaluationContext { /** For transcription only */ selectedRegion?: string actionType: BulkActionType + /** Required amount for full job in base units: seconds (transcription) or characters (translation). */ + nearLimitRequiredAmount?: number serviceUsageData?: ServiceUsageResponse activeBulkActions: BulkActionResponse[] previouslyFilteredSubmissionUuids: Set diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts index 0bf115e990..b951100405 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts @@ -13,6 +13,8 @@ interface UseBulkProcessingAlertsProps { /** Selected region (transcription only) */ selectedRegion?: string fieldXpath: string + /** Required amount for full job in base units: seconds (transcription) or characters (translation). */ + nearLimitRequiredAmount?: number serviceUsageData?: ServiceUsageResponse activeBulkActions: BulkActionResponse[] } @@ -42,6 +44,7 @@ export function useBulkProcessingAlerts(props: UseBulkProcessingAlertsProps): Us selectedLanguage, selectedRegion, fieldXpath, + nearLimitRequiredAmount, serviceUsageData, activeBulkActions, } = props @@ -63,6 +66,7 @@ export function useBulkProcessingAlerts(props: UseBulkProcessingAlertsProps): Us selectedLanguage, selectedRegion, actionType, + nearLimitRequiredAmount, serviceUsageData, activeBulkActions, previouslyFilteredSubmissionUuids: filteredSubmissionUuids, @@ -119,6 +123,7 @@ export function useBulkProcessingAlerts(props: UseBulkProcessingAlertsProps): Us selectedLanguage, selectedRegion, actionType, + nearLimitRequiredAmount, serviceUsageData, activeBulkActions, alertDefinitions, From 74c5b76a3fb7290584427f7a49af2b146c5b280a Mon Sep 17 00:00:00 2001 From: Leszek Date: Thu, 9 Jul 2026 14:50:53 +0200 Subject: [PATCH 06/15] cr fixes --- .../BulkTranscriptionModal.tsx | 16 +++++++--------- .../alerts/alertDefinitions.ts | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index 93d320bb83..1ab2bf1c50 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -29,11 +29,16 @@ import LanguageSelector from '../../../languages/LanguageSelector' import type { LanguageCode } from '../../../languages/languagesStore' import { BulkProcessingWarningModal } from '../../BulkProcessingModals/BulkProcessingWarningModal' import BulkProcessingAlerts from '../alerts/BulkProcessingAlerts' -import { getAlertDefinitions } from '../alerts/alertDefinitions' import { useBulkProcessingAlerts } from '../alerts/useBulkProcessingAlerts' const GOOGLE_TRANSCRIPTION_LANGUAGE_SUPPORT_URL = 'transcription-translation.html#language-list' +function getAlreadyTranscribedMessage(count: number, duration: string): string { + return t('##count## audio files totaling ##duration## already transcribed and will be ignored') + .replace('##count##', String(count)) + .replace('##duration##', duration) +} + export interface BulkTranscriptionModalProps { fieldXpath: string assetUid: string @@ -158,13 +163,6 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { // "already transcribed" warning with duration text calculated in this modal. // All other alerts are rendered exactly as returned by the hook. const activeAlertsWithResolvedMinutes = useMemo(() => { - const alreadyTranscribedAlert = getAlertDefinitions('transcript').find( - (alert) => alert.id === 'already-transcribed', - ) - if (!alreadyTranscribedAlert) { - return activeAlerts - } - const duration = isAlreadyTranscribedDurationLoading || isAlreadyTranscribedDurationError ? '…' @@ -183,7 +181,7 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { return { ...alert, computedValues, - message: alreadyTranscribedAlert.messageTemplate(computedValues), + message: getAlreadyTranscribedMessage(Number(computedValues.count ?? 0), String(computedValues.duration ?? 0)), } }) }, [activeAlerts, alreadyTranscribedDuration, isAlreadyTranscribedDurationError, isAlreadyTranscribedDurationLoading]) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts index 8175cace2a..15173da4cb 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts @@ -75,7 +75,7 @@ export function getAlertDefinitions(actionType: BulkActionType): AlertDefinition .replace('##duration##', String(values.duration ?? formatTimeFromSeconds(0))) : t('##count## transcripts totaling ##characters## characters already translated and will be ignored') .replace('##count##', String(values.count ?? 0)) - .replace('##characters##', String(values.characters ?? formatTimeFromSeconds(0))), + .replace('##characters##', String(values.characters ?? 0)), }, { id: 'no-eligible-submissions', From adeca590de7de5ee7ddd6039814c12a146b533d0 Mon Sep 17 00:00:00 2001 From: Leszek Date: Thu, 9 Jul 2026 15:02:14 +0200 Subject: [PATCH 07/15] fix ts --- .../BulkTranscriptionModal/BulkTranscriptionModal.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index 1ab2bf1c50..defc1bb854 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -181,7 +181,10 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { return { ...alert, computedValues, - message: getAlreadyTranscribedMessage(Number(computedValues.count ?? 0), String(computedValues.duration ?? 0)), + message: getAlreadyTranscribedMessage( + Number(alert.computedValues.count ?? 0), + String(computedValues.duration ?? 0), + ), } }) }, [activeAlerts, alreadyTranscribedDuration, isAlreadyTranscribedDurationError, isAlreadyTranscribedDurationLoading]) From 5579acc42bf47279f6821400c18a5f7d0276ed1c Mon Sep 17 00:00:00 2001 From: Leszek Date: Thu, 9 Jul 2026 17:02:37 +0200 Subject: [PATCH 08/15] cr fixes --- .../BulkTranscriptionModal.tsx | 16 ++++++++- .../BulkTranslationModal.tsx | 33 ++++++++++++++----- .../alerts/BulkProcessingAlerts.stories.tsx | 2 +- .../alerts/alertDefinitions.ts | 8 +++-- .../alerts/alertEvaluators.tests.ts | 2 +- .../alerts/alertEvaluators.ts | 3 +- 6 files changed, 47 insertions(+), 17 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index 61a4c69e6d..7aaebec5a6 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -39,6 +39,12 @@ function getAlreadyTranscribedMessage(count: number, duration: string): string { .replace('##duration##', duration) } +function isAlreadyTranscribedSubmission(submission: SubmissionResponse, sourceRowPath: string): boolean { + const transcript = submission._supplementalDetails?.[sourceRowPath]?.transcript + + return Boolean(transcript?.value || transcript?.pendingReview) +} + export interface BulkTranscriptionModalProps { fieldXpath: string assetUid: string @@ -111,12 +117,20 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { const { sourceRowPath } = getSupplementalPathParts(props.fieldXpath) + // Keep the quota estimate aligned with the rows that will actually be sent. + // The alert hook filters already-transcribed submissions later, so we mirror + // that exclusion here instead of counting every selected row up front. + const transcribableSubmissions = useMemo( + () => props.selectedSubmissions.filter((submission) => !isAlreadyTranscribedSubmission(submission, sourceRowPath)), + [props.selectedSubmissions, sourceRowPath], + ) + const { duration: totalSelectedAudioDuration, isLoading: isTotalSelectedAudioDurationLoading, isError: isTotalSelectedAudioDurationError, } = useCalculateAudioDuration({ - selectedSubmissions: props.selectedSubmissions, + selectedSubmissions: transcribableSubmissions, fieldId: sourceRowPath, assetUid: props.assetUid, }) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx index 37381832de..390b589825 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx @@ -1,6 +1,6 @@ import { Anchor, Group, Stack, Text } from '@mantine/core' import { useQueryClient } from '@tanstack/react-query' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { ACCOUNT_ROUTES } from '#/account/routes.constants' import type { ServerError } from '#/api/ServerError' @@ -102,11 +102,30 @@ export function BulkTranslationModal(props: BulkTranslationModalProps) { const advancedFeatures = advancedFeaturesData?.status === 200 ? advancedFeaturesData.data : [] const suggestedLanguages = getSuggestedLanguages(advancedFeatures) + const { sourceRowPath } = getSupplementalPathParts(props.fieldXpath) + // Use bulk processing alerts hook - const nearLimitRequiredCharacters = props.selectedSubmissions.reduce((sum, sub) => { - const supplementalValue = getSupplementalDetailsContent(sub, props.fieldXpath) || '' - return sum + supplementalValue.length - }, 0) + // Near-limit should reflect only the submissions that still need translation. + // Rows that already have a translation in the selected language will be filtered + // out by the alert pipeline, so we exclude them here as well to keep the limit check aligned. + const nearLimitRequiredCharacters = useMemo(() => { + if (!selectedLanguage) { + return undefined + } + + return props.selectedSubmissions.reduce((sum, sub) => { + const supplementalDetails = sub._supplementalDetails?.[sourceRowPath] + const translation = supplementalDetails?.translation?.[selectedLanguage] + + if (translation?.value) { + // This row is already translated in the chosen language and won't consume MT quota. + return sum + } + + const supplementalValue = getSupplementalDetailsContent(sub, props.fieldXpath) || '' + return sum + supplementalValue.length + }, 0) + }, [props.selectedSubmissions, props.fieldXpath, selectedLanguage, sourceRowPath]) const { activeAlerts, hasErrors, hasBlockingError, eligibleSubmissions } = useBulkProcessingAlerts({ actionType: 'translation', @@ -129,10 +148,6 @@ export function BulkTranslationModal(props: BulkTranslationModalProps) { const eligibleSubmissionUuids = eligibleSubmissions.map((submission) => submission._uuid) const handleStartTranslation = () => { - // Extract the source row path from the transcript column path - // e.g., "_supplementalDetails/q1/transcript_en" -> "q1" - const { sourceRowPath } = getSupplementalPathParts(props.fieldXpath) - // Use eligibleSubmissionUuids from the alerts hook to filter out submissions // that have been flagged by warning evaluators (e.g., already translated, no source) createBulkTranslation({ diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/BulkProcessingAlerts.stories.tsx b/jsapp/js/components/submissions/BulkProcessingModals/alerts/BulkProcessingAlerts.stories.tsx index d1a1ceb353..27ebdcba76 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/BulkProcessingAlerts.stories.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/BulkProcessingAlerts.stories.tsx @@ -41,7 +41,7 @@ function createMockAlert( // Mock alerts const mockErrorAlert = createMockAlert('reached-limit', {}) -const mockNearLimitAlert = createMockAlert('near-limit', { remainingMinutes: 5 }) +const mockNearLimitAlert = createMockAlert('near-limit', { remainingSeconds: 5 }) const mockWarningAlreadyTranscribed = createMockAlert('already-transcribed', { count: 3, duration: '15 minutes' }) const mockWarningNoSource = createMockAlert('no-source', { count: 2 }) const mockWarningConflicting = createMockAlert('conflicting-job', {}) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts index 15173da4cb..fedcc0b0eb 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts @@ -36,11 +36,13 @@ export function getAlertDefinitions(actionType: BulkActionType): AlertDefinition type: 'error', evaluator: evaluateNearLimit, messageTemplate: (values) => { - const remaining = isTranscription ? (values.remainingMinutes ?? '0') : (values.remainingCharacters ?? '0') + const remaining = isTranscription + ? formatTimeFromSeconds(Number(values.remainingSeconds ?? 0)) + : (values.remainingCharacters ?? '0') return isTranscription ? t( - '##remainingMinutes## minutes of automated transcription left, that is not enough to process all selected submissions. Please select fewer files, purchase an add-on, or upgrade your plan.', - ).replace('##remainingMinutes##', String(remaining)) + '##remainingDuration## of automated transcription left, that is not enough to process all selected submissions. Please select fewer files, purchase an add-on, or upgrade your plan.', + ).replace('##remainingDuration##', String(remaining)) : t( '##remainingCharacters## characters of automated translation left, that is not enough to process all selected submissions. Please select fewer files, purchase an add-on, or upgrade your plan.', ).replace('##remainingCharacters##', String(remaining)) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts index 539efaffef..dfca03e9c7 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts @@ -195,7 +195,7 @@ describe('evaluateNearLimit', () => { expect(result.type).to.equal('error') expect(result.filteredSubmissionUuids).to.deep.equal([]) expect(result.computedValues).to.deep.equal({ - remainingMinutes: 0, + remainingSeconds: 30, }) }) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts index b0f20c9aee..d8c0102f93 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts @@ -1,5 +1,4 @@ import { getSupplementalPathParts } from '#/components/processing/processingUtils' -import { convertSecondsToMinutes } from '#/utils' import type { AlertEvaluationContext, AlertEvaluationResult } from './types' import { createInactiveResult } from './utils' @@ -56,7 +55,7 @@ export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluat const computedValues = actionType === 'transcript' ? { - remainingMinutes: convertSecondsToMinutes(remainingAmount), + remainingSeconds: remainingAmount, } : { remainingCharacters: remainingAmount, From dfb1c878d29bbdd16a37093a484e5e422309029f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:34:03 +0000 Subject: [PATCH 09/15] fix bulk alert singular copy --- .../submissions/BulkProcessingModals/alerts/alertDefinitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts index 0b3c0acb50..60f185de0a 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertDefinitions.ts @@ -63,7 +63,7 @@ export function getAlertDefinitions(actionType: BulkActionType): AlertDefinition messageTemplate: ({ count = 0 }) => isTranscription ? (count === 1 - ? t('#1 submission is missing audio file and will be ignored') + ? t('1 submission is missing audio file and will be ignored') : t('##count## submissions are missing audio file and will be ignored') ).replace('##count##', String(count)) : (count === 1 From 3541a3bddde70b88326dba4b4bd785ab90ad4201 Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 13 Jul 2026 10:50:15 +0200 Subject: [PATCH 10/15] biome fix --- .../BulkTranscriptionModal/BulkTranscriptionModal.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index bd8553ec1b..e5e697d0dd 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -34,9 +34,10 @@ import { useBulkProcessingAlerts } from '../alerts/useBulkProcessingAlerts' const GOOGLE_TRANSCRIPTION_LANGUAGE_SUPPORT_URL = 'transcription-translation.html#language-list' function getAlreadyTranscribedMessage(count: number, duration: string): string { - return (count === 1 - ? t('1 audio file totaling ##duration## already transcribed and will be ignored') - : t('##count## audio files totaling ##duration## already transcribed and will be ignored') + return ( + count === 1 + ? t('1 audio file totaling ##duration## already transcribed and will be ignored') + : t('##count## audio files totaling ##duration## already transcribed and will be ignored') ) .replace('##count##', String(count)) .replace('##duration##', duration) From 1bed4f2b0a61536c2bed74bf9fe0231949def2da Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 13 Jul 2026 11:16:32 +0200 Subject: [PATCH 11/15] biome fix --- .../BulkProcessingModals/alerts/alertEvaluators.tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts index 23b5c9c3bf..ecea12df4c 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts @@ -2,6 +2,7 @@ import { expect } from 'chai' import { ActionIdEnum } from '#/api/models/actionIdEnum' import { BulkActionResponseStatusEnum } from '#/api/models/bulkActionResponseStatusEnum' import assetDataFactory from '#/endpoints/assetData.factory' +import bulkActionFactory from '#/endpoints/bulkAction.factory' import { asrExceeded, asrNearLimit, mtExceeded, mtNearLimit, withinLimits } from '#/endpoints/serviceUsage.factory' import { evaluateAlreadyTranscribed, @@ -12,7 +13,6 @@ import { evaluateNoSource, evaluateReachedLimit, } from './alertEvaluators' -import bulkActionFactory from '#/endpoints/bulkAction.factory' import type { AlertEvaluationContext } from './types' describe('evaluateNoEligibleSubmissions', () => { From 01752046edd77df75cef53bf8812d14d4b2475b3 Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 13 Jul 2026 11:34:07 +0200 Subject: [PATCH 12/15] cr fix --- .../BulkTranscriptionModal/BulkTranscriptionModal.tsx | 8 -------- .../BulkTranslationModal/BulkTranslationModal.tsx | 8 -------- .../BulkProcessingModals/alerts/alertEvaluators.ts | 6 ++++++ 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index 1eb23c7ae4..ed7f58309c 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -268,7 +268,6 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { {!showWarningModal && ( - {/* Legacy alert - will be removed once audio duration evaluators are implemented see DEV-1399 */} {isAudioDurationError && audioDurationErrorMesssage && ( {audioDurationErrorMesssage} @@ -308,13 +307,6 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { - {/* Legacy alert - will be removed once evaluators are implemented */} - {hasExceededLimit && activeAlertsWithResolvedMinutes.length === 0 && ( - - {t("You've reached your automatic transcription limit. Please purchase an add‑on to continue.")} - - )} - {t('Automatic transcription is provided by Google Cloud Platform.')}   diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx index 390b589825..e9002a4fcb 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx @@ -16,7 +16,6 @@ import { useOrganizationsServiceUsageRetrieve, } from '#/api/react-query/user-team-organization-usage' import ButtonNew from '#/components/common/ButtonNew' -import Alert from '#/components/common/alert' import LanguageSelector from '#/components/languages/LanguageSelector' import type { LanguageCode } from '#/components/languages/languagesStore' import { getSuggestedLanguages } from '#/components/processing/common/utils' @@ -206,13 +205,6 @@ export function BulkTranslationModal(props: BulkTranslationModalProps) { - {/* Legacy alert - will be removed once evaluators are implemented */} - {hasExceededLimit && activeAlerts.length === 0 && ( - - {t("You've reached your automatic translation limit. Please purchase an add‑on to continue.")} - - )} - {t('Automatic translation is provided by Google Cloud Platform.')}   diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts index acfb2e5713..c54b604aa5 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts @@ -50,6 +50,12 @@ export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluat const remainingAmount = balance.balance_value + // Don't show this alert if: + // 1. remainingAmount <= 0 — no quota left (the reached-limit alert handles this, runs first) + // 2. remainingAmount >= nearLimitRequiredAmount — enough quota to process everything + // + // Show only when 0 < remainingAmount < nearLimitRequiredAmount. + // That's when you have some quota but not enough for all the submissions you selected. if (remainingAmount <= 0 || remainingAmount >= nearLimitRequiredAmount) { return createInactiveResult('error') } From d9541666957e42f4bf31beba72a5e5a89fa3298b Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 13 Jul 2026 14:49:31 +0200 Subject: [PATCH 13/15] simplify not shown alert evaluator result (was object, now is null) --- .../alerts/alertEvaluators.tests.ts | 255 ++++++++---------- .../alerts/alertEvaluators.ts | 74 ++--- .../BulkProcessingModals/alerts/types.ts | 6 +- .../alerts/useBulkProcessingAlerts.ts | 2 +- .../BulkProcessingModals/alerts/utils.ts | 14 - 5 files changed, 149 insertions(+), 202 deletions(-) delete mode 100644 jsapp/js/components/submissions/BulkProcessingModals/alerts/utils.ts diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts index ecea12df4c..dd9cbe6aab 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts @@ -38,10 +38,10 @@ describe('evaluateNoEligibleSubmissions', () => { const result = evaluateNoEligibleSubmissions(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('error') - expect(result.filteredSubmissionUuids).to.deep.equal([]) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.type).to.equal('error') + expect(result?.filteredSubmissionUuids).to.deep.equal([]) + expect(result?.computedValues).to.deep.equal({ totalCount: 3, filteredCount: 3, }) @@ -55,13 +55,7 @@ describe('evaluateNoEligibleSubmissions', () => { const result = evaluateNoEligibleSubmissions(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('error') - expect(result.filteredSubmissionUuids).to.deep.equal([]) - expect(result.computedValues).to.deep.equal({ - totalCount: 3, - filteredCount: 2, - }) + expect(result).to.equal(null) }) it('should not show alert when no submissions are filtered', () => { @@ -72,13 +66,7 @@ describe('evaluateNoEligibleSubmissions', () => { const result = evaluateNoEligibleSubmissions(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('error') - expect(result.filteredSubmissionUuids).to.deep.equal([]) - expect(result.computedValues).to.deep.equal({ - totalCount: 3, - filteredCount: 0, - }) + expect(result).to.equal(null) }) it('should handle empty submissions array', () => { @@ -90,9 +78,9 @@ describe('evaluateNoEligibleSubmissions', () => { const result = evaluateNoEligibleSubmissions(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('error') - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.type).to.equal('error') + expect(result?.computedValues).to.deep.equal({ totalCount: 0, filteredCount: 0, }) @@ -122,9 +110,7 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should not show alert when ongoing jobs are for different field', () => { @@ -141,9 +127,7 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should not show alert when jobs are completed', () => { @@ -160,9 +144,7 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should show alert for transcription when ongoing transcription job conflicts', () => { @@ -180,10 +162,10 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) + expect(result?.computedValues).to.deep.equal({ count: 2, conflictingJobCount: 1, }) @@ -204,10 +186,10 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-3']) - expect(result.computedValues.count).to.equal(1) + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-3']) + expect(result?.computedValues.count).to.equal(1) }) it('should show alert for translation when ongoing translation job conflicts with same language', () => { @@ -227,9 +209,9 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1']) + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1']) }) it('should not show alert for translation when ongoing translation job is for different language', () => { @@ -249,9 +231,7 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should show alert for translation when ongoing transcription job conflicts', () => { @@ -271,9 +251,9 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2']) + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-2']) }) it('should handle multiple conflicting jobs', () => { @@ -297,10 +277,10 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.have.members(['uuid-1', 'uuid-2', 'uuid-3']) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.have.members(['uuid-1', 'uuid-2', 'uuid-3']) + expect(result?.computedValues).to.deep.equal({ count: 3, conflictingJobCount: 2, }) @@ -321,9 +301,7 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should not show alert for translation when ongoing translation job is for different field', () => { @@ -343,9 +321,7 @@ describe('evaluateConflictingJob', () => { const result = evaluateConflictingJob(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) }) @@ -368,9 +344,9 @@ describe('evaluateReachedLimit', () => { const result = evaluateReachedLimit(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('error') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.not.equal(null) + expect(result?.type).to.equal('error') + expect(result?.filteredSubmissionUuids).to.deep.equal([]) }) it('should not show alert when transcription quota is not exceeded', () => { @@ -381,8 +357,7 @@ describe('evaluateReachedLimit', () => { const result = evaluateReachedLimit(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('error') + expect(result).to.equal(null) }) it('should show alert when translation quota is exceeded', () => { @@ -394,9 +369,9 @@ describe('evaluateReachedLimit', () => { const result = evaluateReachedLimit(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('error') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.not.equal(null) + expect(result?.type).to.equal('error') + expect(result?.filteredSubmissionUuids).to.deep.equal([]) }) it('should not show alert when translation quota is not exceeded', () => { @@ -408,8 +383,7 @@ describe('evaluateReachedLimit', () => { const result = evaluateReachedLimit(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('error') + expect(result).to.equal(null) }) it('should not show alert when serviceUsageData is missing', () => { @@ -420,8 +394,7 @@ describe('evaluateReachedLimit', () => { const result = evaluateReachedLimit(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('error') + expect(result).to.equal(null) }) }) @@ -445,10 +418,10 @@ describe('evaluateNearLimit', () => { const result = evaluateNearLimit(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('error') - expect(result.filteredSubmissionUuids).to.deep.equal([]) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.type).to.equal('error') + expect(result?.filteredSubmissionUuids).to.deep.equal([]) + expect(result?.computedValues).to.deep.equal({ remainingSeconds: 30, }) }) @@ -462,8 +435,7 @@ describe('evaluateNearLimit', () => { const result = evaluateNearLimit(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('error') + expect(result).to.equal(null) }) it('should not show alert when balance is exceeded', () => { @@ -475,8 +447,7 @@ describe('evaluateNearLimit', () => { const result = evaluateNearLimit(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('error') + expect(result).to.equal(null) }) it('should show alert for translation when remaining characters are below required amount', () => { @@ -489,10 +460,10 @@ describe('evaluateNearLimit', () => { const result = evaluateNearLimit(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('error') - expect(result.filteredSubmissionUuids).to.deep.equal([]) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.type).to.equal('error') + expect(result?.filteredSubmissionUuids).to.deep.equal([]) + expect(result?.computedValues).to.deep.equal({ remainingCharacters: 2500, }) }) @@ -506,8 +477,7 @@ describe('evaluateNearLimit', () => { const result = evaluateNearLimit(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('error') + expect(result).to.equal(null) }) }) @@ -543,8 +513,7 @@ describe('evaluateAlreadyTranslated', () => { const result = evaluateAlreadyTranslated(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') + expect(result).to.equal(null) }) it('should not show alert when no submissions have translations', () => { @@ -557,9 +526,7 @@ describe('evaluateAlreadyTranslated', () => { const result = evaluateAlreadyTranslated(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should show alert when submissions have existing translations', () => { @@ -594,10 +561,10 @@ describe('evaluateAlreadyTranslated', () => { const result = evaluateAlreadyTranslated(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) + expect(result?.computedValues).to.deep.equal({ count: 2, characters: 7 + 9, // 'Bonjour' + 'Au revoir' }) @@ -634,9 +601,9 @@ describe('evaluateAlreadyTranslated', () => { const result = evaluateAlreadyTranslated(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2']) - expect(result.computedValues.count).to.equal(1) + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-2']) + expect(result?.computedValues.count).to.equal(1) }) it('should not flag submissions with empty translation value', () => { @@ -670,8 +637,7 @@ describe('evaluateAlreadyTranslated', () => { const result = evaluateAlreadyTranslated(context) - expect(result.shouldShow).to.equal(false) - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should skip submissions already filtered by previous evaluators', () => { @@ -706,9 +672,9 @@ describe('evaluateAlreadyTranslated', () => { const result = evaluateAlreadyTranslated(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2']) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-2']) + expect(result?.computedValues).to.deep.equal({ count: 1, characters: 9, // Only 'Au revoir' }) @@ -735,8 +701,7 @@ describe('evaluateAlreadyTranslated', () => { const result = evaluateAlreadyTranslated(context) - expect(result.shouldShow).to.equal(false) - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should work with direct field xpath (not transcript column xpath)', () => { @@ -761,9 +726,9 @@ describe('evaluateAlreadyTranslated', () => { const result = evaluateAlreadyTranslated(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1']) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1']) + expect(result?.computedValues).to.deep.equal({ count: 1, characters: 7, }) @@ -789,13 +754,7 @@ describe('evaluateAlreadyTranscribed', () => { const result = evaluateAlreadyTranscribed(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) - expect(result.computedValues).to.deep.equal({ - count: 0, - duration: 0, - }) + expect(result).to.equal(null) }) it('should show alert when submissions have existing transcripts in any language', () => { @@ -832,10 +791,10 @@ describe('evaluateAlreadyTranscribed', () => { const result = evaluateAlreadyTranscribed(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) + expect(result?.computedValues).to.deep.equal({ count: 2, duration: 0, }) @@ -863,9 +822,9 @@ describe('evaluateAlreadyTranscribed', () => { const result = evaluateAlreadyTranscribed(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1']) - expect(result.computedValues.count).to.equal(1) + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1']) + expect(result?.computedValues.count).to.equal(1) }) it('should skip submissions already filtered by previous evaluators', () => { @@ -902,9 +861,9 @@ describe('evaluateAlreadyTranscribed', () => { const result = evaluateAlreadyTranscribed(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2']) - expect(result.computedValues).to.deep.equal({ + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-2']) + expect(result?.computedValues).to.deep.equal({ count: 1, duration: 0, }) @@ -960,9 +919,7 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should show alert when submissions are missing audio attachments', () => { @@ -992,10 +949,10 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2', 'uuid-3']) - expect(result.computedValues.count).to.equal(2) + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-2', 'uuid-3']) + expect(result?.computedValues.count).to.equal(2) }) it('should ignore deleted attachments', () => { @@ -1023,8 +980,8 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1']) + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1']) }) it('should check correct field xpath', () => { @@ -1052,8 +1009,8 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1']) + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1']) }) it('should skip submissions already filtered by previous evaluators', () => { @@ -1070,9 +1027,9 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2']) - expect(result.computedValues.count).to.equal(1) + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-2']) + expect(result?.computedValues.count).to.equal(1) }) }) @@ -1112,9 +1069,7 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(false) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal([]) + expect(result).to.equal(null) }) it('should show alert when submissions are missing transcripts', () => { @@ -1143,10 +1098,10 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(true) - expect(result.type).to.equal('warning') - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2', 'uuid-3']) - expect(result.computedValues.count).to.equal(2) + expect(result).to.not.equal(null) + expect(result?.type).to.equal('warning') + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-2', 'uuid-3']) + expect(result?.computedValues.count).to.equal(2) }) it('should flag submissions with empty transcript value', () => { @@ -1176,8 +1131,8 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1', 'uuid-2']) }) it('should check correct field xpath', () => { @@ -1199,8 +1154,8 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-1']) + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-1']) }) it('should skip submissions already filtered by previous evaluators', () => { @@ -1214,9 +1169,9 @@ describe('evaluateNoSource', () => { const result = evaluateNoSource(context) - expect(result.shouldShow).to.equal(true) - expect(result.filteredSubmissionUuids).to.deep.equal(['uuid-2']) - expect(result.computedValues.count).to.equal(1) + expect(result).to.not.equal(null) + expect(result?.filteredSubmissionUuids).to.deep.equal(['uuid-2']) + expect(result?.computedValues.count).to.equal(1) }) }) }) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts index c54b604aa5..14d31d8a4f 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts @@ -2,25 +2,28 @@ import { ActionIdEnum } from '#/api/models/actionIdEnum' import { BulkActionResponseStatusEnum } from '#/api/models/bulkActionResponseStatusEnum' import { getSupplementalPathParts } from '#/components/processing/processingUtils' import type { AlertEvaluationContext, AlertEvaluationResult } from './types' -import { createInactiveResult } from './utils' /** * Checks if user has reached their quota limit (0 remaining) */ -export function evaluateReachedLimit(context: AlertEvaluationContext): AlertEvaluationResult { +export function evaluateReachedLimit(context: AlertEvaluationContext): AlertEvaluationResult | null { const { actionType, serviceUsageData } = context // Can't evaluate without service usage data if (!serviceUsageData?.balances) { - return createInactiveResult('error') + return null } // Check the appropriate balance based on action type const balance = actionType === 'transcript' ? serviceUsageData.balances.asr_seconds : serviceUsageData.balances.mt_characters + const exceeded = balance?.exceeded || false + if (!exceeded) { + return null + } + return { - shouldShow: balance?.exceeded || false, type: 'error', filteredSubmissionUuids: [], computedValues: {}, @@ -30,22 +33,22 @@ export function evaluateReachedLimit(context: AlertEvaluationContext): AlertEval /** * Checks if remaining quota is less than required but greater than 0 */ -export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluationResult { +export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluationResult | null { const { actionType, serviceUsageData, nearLimitRequiredAmount } = context if (!serviceUsageData?.balances || nearLimitRequiredAmount === undefined) { - return createInactiveResult('error') + return null } if (nearLimitRequiredAmount <= 0) { - return createInactiveResult('error') + return null } const balance = actionType === 'transcript' ? serviceUsageData.balances.asr_seconds : serviceUsageData.balances.mt_characters if (!balance) { - return createInactiveResult('error') + return null } const remainingAmount = balance.balance_value @@ -57,7 +60,7 @@ export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluat // Show only when 0 < remainingAmount < nearLimitRequiredAmount. // That's when you have some quota but not enough for all the submissions you selected. if (remainingAmount <= 0 || remainingAmount >= nearLimitRequiredAmount) { - return createInactiveResult('error') + return null } const computedValues = @@ -70,7 +73,6 @@ export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluat } return { - shouldShow: true, type: 'error', filteredSubmissionUuids: [], computedValues, @@ -85,7 +87,7 @@ export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluat * - Ongoing translation jobs on the same field AND same target language (write-locked output) * - Ongoing transcription jobs on the input transcript field (write-locked input) */ -export function evaluateConflictingJob(context: AlertEvaluationContext): AlertEvaluationResult { +export function evaluateConflictingJob(context: AlertEvaluationContext): AlertEvaluationResult | null { const { activeBulkActions, fieldXpath, actionType, submissions, selectedLanguage } = context // Filter to only ongoing jobs (pending or in_progress) @@ -96,12 +98,7 @@ export function evaluateConflictingJob(context: AlertEvaluationContext): AlertEv ) if (ongoingJobs.length === 0) { - return { - shouldShow: false, - type: 'warning', - filteredSubmissionUuids: [], - computedValues: {}, - } + return null } // Find conflicting jobs based on action type @@ -135,12 +132,7 @@ export function evaluateConflictingJob(context: AlertEvaluationContext): AlertEv } if (conflictingJobs.length === 0) { - return { - shouldShow: false, - type: 'warning', - filteredSubmissionUuids: [], - computedValues: {}, - } + return null } // Collect all submission UUIDs from conflicting jobs @@ -154,8 +146,11 @@ export function evaluateConflictingJob(context: AlertEvaluationContext): AlertEv .filter((submission) => conflictingUuids.has(submission._uuid)) .map((submission) => submission._uuid) + if (filteredSubmissionUuids.length === 0) { + return null + } + return { - shouldShow: filteredSubmissionUuids.length > 0, type: 'warning', filteredSubmissionUuids, computedValues: { @@ -169,7 +164,7 @@ export function evaluateConflictingJob(context: AlertEvaluationContext): AlertEv * Checks for submissions missing audio attachments (transcription) * or missing transcripts (translation) */ -export function evaluateNoSource(context: AlertEvaluationContext): AlertEvaluationResult { +export function evaluateNoSource(context: AlertEvaluationContext): AlertEvaluationResult | null { const { submissions, fieldXpath, actionType, previouslyFilteredSubmissionUuids } = context const missingSource: string[] = [] @@ -203,8 +198,11 @@ export function evaluateNoSource(context: AlertEvaluationContext): AlertEvaluati } }) + if (missingSource.length === 0) { + return null + } + return { - shouldShow: missingSource.length > 0, type: 'warning', filteredSubmissionUuids: missingSource, computedValues: { @@ -216,7 +214,7 @@ export function evaluateNoSource(context: AlertEvaluationContext): AlertEvaluati /** * Checks for submissions with existing transcripts */ -export function evaluateAlreadyTranscribed(context: AlertEvaluationContext): AlertEvaluationResult { +export function evaluateAlreadyTranscribed(context: AlertEvaluationContext): AlertEvaluationResult | null { const { submissions, fieldXpath, previouslyFilteredSubmissionUuids } = context const { sourceRowPath } = getSupplementalPathParts(fieldXpath) @@ -236,8 +234,11 @@ export function evaluateAlreadyTranscribed(context: AlertEvaluationContext): Ale } }) + if (alreadyTranscribed.length === 0) { + return null + } + return { - shouldShow: alreadyTranscribed.length > 0, type: 'warning', filteredSubmissionUuids: alreadyTranscribed, // The exact duration (in minutes) is resolved in the transcription modal @@ -252,12 +253,12 @@ export function evaluateAlreadyTranscribed(context: AlertEvaluationContext): Ale /** * Checks for submissions with existing translations in the selected language */ -export function evaluateAlreadyTranslated(context: AlertEvaluationContext): AlertEvaluationResult { +export function evaluateAlreadyTranslated(context: AlertEvaluationContext): AlertEvaluationResult | null { const { submissions, fieldXpath, selectedLanguage, previouslyFilteredSubmissionUuids } = context // Can't evaluate without a selected language if (!selectedLanguage) { - return createInactiveResult('warning') + return null } const { sourceRowPath } = getSupplementalPathParts(fieldXpath) @@ -282,8 +283,11 @@ export function evaluateAlreadyTranslated(context: AlertEvaluationContext): Aler } }) + if (alreadyTranslated.length === 0) { + return null + } + return { - shouldShow: alreadyTranslated.length > 0, type: 'warning', filteredSubmissionUuids: alreadyTranslated, computedValues: { @@ -296,10 +300,14 @@ export function evaluateAlreadyTranslated(context: AlertEvaluationContext): Aler /** * Checks if all submissions have been filtered out by previous evaluators */ -export function evaluateNoEligibleSubmissions(context: AlertEvaluationContext): AlertEvaluationResult { +export function evaluateNoEligibleSubmissions(context: AlertEvaluationContext): AlertEvaluationResult | null { const eligibleCount = context.submissions.length - context.previouslyFilteredSubmissionUuids.size + + if (eligibleCount > 0) { + return null + } + return { - shouldShow: eligibleCount === 0, type: 'error', filteredSubmissionUuids: [], computedValues: { diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts index 63724ef704..36d874513a 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts @@ -31,11 +31,9 @@ export interface AlertEvaluationContext { } /** - * Result returned by alert evaluators + * A "show alert" result returned by alert evaluators (otherwise it returns `null`) */ export interface AlertEvaluationResult { - /** Whether this alert should be displayed */ - shouldShow: boolean type: AlertSeverity /** Submission Uuids filtered out by this evaluator */ filteredSubmissionUuids: string[] @@ -51,7 +49,7 @@ export interface AlertDefinition { /** Unique alert identifier */ id: string type: AlertSeverity - evaluator: (context: AlertEvaluationContext) => AlertEvaluationResult + evaluator: (context: AlertEvaluationContext) => AlertEvaluationResult | null messageTemplate: (values: Record) => string } diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts index b951100405..49d8e5bdb9 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts @@ -76,7 +76,7 @@ export function useBulkProcessingAlerts(props: UseBulkProcessingAlertsProps): Us for (const alertDef of alertDefinitions) { const result = alertDef.evaluator(context) - if (result.shouldShow) { + if (result) { // Add filtered submission uuids to the set (for warnings) if (result.type === 'warning') { result.filteredSubmissionUuids.forEach((uuid) => filteredSubmissionUuids.add(uuid)) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/utils.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/utils.ts deleted file mode 100644 index 239ccca309..0000000000 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { AlertEvaluationResult, AlertSeverity } from './types' - -/** - * Helper function to create an inactive alert result - * Use this when an evaluator determines no alert should be shown - */ -export function createInactiveResult(type: AlertSeverity = 'warning'): AlertEvaluationResult { - return { - shouldShow: false, - type, - filteredSubmissionUuids: [], - computedValues: {}, - } -} From 4f4008775ca68b5833c0ed101bbd5a188a324cf8 Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 13 Jul 2026 14:51:28 +0200 Subject: [PATCH 14/15] rename prop --- .../alerts/alertEvaluators.tests.ts | 10 +++++----- .../BulkProcessingModals/alerts/alertEvaluators.ts | 12 ++++++------ .../submissions/BulkProcessingModals/alerts/types.ts | 4 ++-- .../alerts/useBulkProcessingAlerts.ts | 8 ++++---- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts index dd9cbe6aab..a8ca6b5b9a 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.tests.ts @@ -413,7 +413,7 @@ describe('evaluateNearLimit', () => { const context: AlertEvaluationContext = { ...baseContext, serviceUsageData: asrNearLimit(95), - nearLimitRequiredAmount: 120, + requiredAmount: 120, } const result = evaluateNearLimit(context) @@ -430,7 +430,7 @@ describe('evaluateNearLimit', () => { const context: AlertEvaluationContext = { ...baseContext, serviceUsageData: asrNearLimit(95), - nearLimitRequiredAmount: 20, + requiredAmount: 20, } const result = evaluateNearLimit(context) @@ -442,7 +442,7 @@ describe('evaluateNearLimit', () => { const context: AlertEvaluationContext = { ...baseContext, serviceUsageData: asrExceeded(), - nearLimitRequiredAmount: 120, + requiredAmount: 120, } const result = evaluateNearLimit(context) @@ -455,7 +455,7 @@ describe('evaluateNearLimit', () => { ...baseContext, actionType: 'translation', serviceUsageData: mtNearLimit(95), - nearLimitRequiredAmount: 3000, + requiredAmount: 3000, } const result = evaluateNearLimit(context) @@ -472,7 +472,7 @@ describe('evaluateNearLimit', () => { const context: AlertEvaluationContext = { ...baseContext, serviceUsageData: asrNearLimit(95), - nearLimitRequiredAmount: undefined, + requiredAmount: undefined, } const result = evaluateNearLimit(context) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts index 14d31d8a4f..f4a9e659d6 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/alertEvaluators.ts @@ -34,13 +34,13 @@ export function evaluateReachedLimit(context: AlertEvaluationContext): AlertEval * Checks if remaining quota is less than required but greater than 0 */ export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluationResult | null { - const { actionType, serviceUsageData, nearLimitRequiredAmount } = context + const { actionType, serviceUsageData, requiredAmount } = context - if (!serviceUsageData?.balances || nearLimitRequiredAmount === undefined) { + if (!serviceUsageData?.balances || requiredAmount === undefined) { return null } - if (nearLimitRequiredAmount <= 0) { + if (requiredAmount <= 0) { return null } @@ -55,11 +55,11 @@ export function evaluateNearLimit(context: AlertEvaluationContext): AlertEvaluat // Don't show this alert if: // 1. remainingAmount <= 0 — no quota left (the reached-limit alert handles this, runs first) - // 2. remainingAmount >= nearLimitRequiredAmount — enough quota to process everything + // 2. remainingAmount >= requiredAmount — enough quota to process everything // - // Show only when 0 < remainingAmount < nearLimitRequiredAmount. + // Show only when 0 < remainingAmount < requiredAmount. // That's when you have some quota but not enough for all the submissions you selected. - if (remainingAmount <= 0 || remainingAmount >= nearLimitRequiredAmount) { + if (remainingAmount <= 0 || remainingAmount >= requiredAmount) { return null } diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts index 36d874513a..df916c9ac8 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/types.ts @@ -23,8 +23,8 @@ export interface AlertEvaluationContext { /** For transcription only */ selectedRegion?: string actionType: BulkActionType - /** Required amount for full job in base units: seconds (transcription) or characters (translation). */ - nearLimitRequiredAmount?: number + /** Required amount to process all selected submissions in base units: seconds (transcription) or characters (translation). */ + requiredAmount?: number serviceUsageData?: ServiceUsageResponse activeBulkActions: BulkActionResponse[] previouslyFilteredSubmissionUuids: Set diff --git a/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts b/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts index 49d8e5bdb9..8860cda5be 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts +++ b/jsapp/js/components/submissions/BulkProcessingModals/alerts/useBulkProcessingAlerts.ts @@ -14,7 +14,7 @@ interface UseBulkProcessingAlertsProps { selectedRegion?: string fieldXpath: string /** Required amount for full job in base units: seconds (transcription) or characters (translation). */ - nearLimitRequiredAmount?: number + requiredAmount?: number serviceUsageData?: ServiceUsageResponse activeBulkActions: BulkActionResponse[] } @@ -44,7 +44,7 @@ export function useBulkProcessingAlerts(props: UseBulkProcessingAlertsProps): Us selectedLanguage, selectedRegion, fieldXpath, - nearLimitRequiredAmount, + requiredAmount, serviceUsageData, activeBulkActions, } = props @@ -66,7 +66,7 @@ export function useBulkProcessingAlerts(props: UseBulkProcessingAlertsProps): Us selectedLanguage, selectedRegion, actionType, - nearLimitRequiredAmount, + requiredAmount, serviceUsageData, activeBulkActions, previouslyFilteredSubmissionUuids: filteredSubmissionUuids, @@ -123,7 +123,7 @@ export function useBulkProcessingAlerts(props: UseBulkProcessingAlertsProps): Us selectedLanguage, selectedRegion, actionType, - nearLimitRequiredAmount, + requiredAmount, serviceUsageData, activeBulkActions, alertDefinitions, From a4f10c3da215f1d878041ed211b3d8b6baeb571d Mon Sep 17 00:00:00 2001 From: Leszek Date: Mon, 13 Jul 2026 14:52:34 +0200 Subject: [PATCH 15/15] rename pt 2 --- .../BulkTranscriptionModal/BulkTranscriptionModal.tsx | 4 ++-- .../BulkTranslationModal/BulkTranslationModal.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx index ed7f58309c..7ce67610a7 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranscriptionModal/BulkTranscriptionModal.tsx @@ -139,7 +139,7 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { assetUid: props.assetUid, }) - const nearLimitRequiredSeconds = + const requiredSeconds = isTotalSelectedAudioDurationLoading || isTotalSelectedAudioDurationError ? undefined : totalSelectedAudioDuration const { activeAlerts, hasErrors, hasBlockingError, eligibleSubmissions } = useBulkProcessingAlerts({ @@ -148,7 +148,7 @@ export function BulkTranscriptionModal(props: BulkTranscriptionModalProps) { selectedLanguage: selectedLanguage || undefined, selectedRegion: selectedRegion || undefined, fieldXpath: props.fieldXpath, - nearLimitRequiredAmount: nearLimitRequiredSeconds, + requiredAmount: requiredSeconds, serviceUsageData: serviceUsageData || undefined, activeBulkActions: props.activeBulkActions, }) diff --git a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx index e9002a4fcb..eabeae34dd 100644 --- a/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx +++ b/jsapp/js/components/submissions/BulkProcessingModals/BulkTranslationModal/BulkTranslationModal.tsx @@ -107,7 +107,7 @@ export function BulkTranslationModal(props: BulkTranslationModalProps) { // Near-limit should reflect only the submissions that still need translation. // Rows that already have a translation in the selected language will be filtered // out by the alert pipeline, so we exclude them here as well to keep the limit check aligned. - const nearLimitRequiredCharacters = useMemo(() => { + const requiredCharacters = useMemo(() => { if (!selectedLanguage) { return undefined } @@ -131,7 +131,7 @@ export function BulkTranslationModal(props: BulkTranslationModalProps) { selectedSubmissions: props.selectedSubmissions, selectedLanguage: selectedLanguage || undefined, fieldXpath: props.fieldXpath, - nearLimitRequiredAmount: nearLimitRequiredCharacters, + requiredAmount: requiredCharacters, serviceUsageData: serviceUsageData || undefined, activeBulkActions: props.activeBulkActions, })