diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 0cd97ef55eb6..008a687a0efe 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -4,6 +4,8 @@ import {FlatList} from 'react-native-gesture-handler'; import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import {hasHoverSupport} from '@libs/DeviceCapabilities'; @@ -23,6 +25,7 @@ function BaseAutoCompleteSuggestions({ measuredHeightOfSuggestionRows, }: ExternalProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); const prevRowHeightRef = useRef(measuredHeightOfSuggestionRows); @@ -41,6 +44,7 @@ function BaseAutoCompleteSuggestions({ onLongPress={() => {}} accessibilityLabel={accessibilityLabelExtractor(item, index)} role={CONST.ROLE.MENUITEM} + sentryLabel="AutoCompleteSuggestion-Item" > {renderSuggestionMenuItem(item, index)} @@ -92,6 +96,7 @@ function BaseAutoCompleteSuggestions({ return ( { if (hasHoverSupport()) { return; @@ -99,6 +104,7 @@ function BaseAutoCompleteSuggestions({ e.preventDefault(); }} > + {translate('common.suggestionsAvailable', suggestions.length)} ({ setShouldDisableHoverStyle = () => {}, }: SelectionListProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); const isFocused = useIsFocused(); const scrollEnabled = useScrollEnabled(); const {singleExecution} = useSingleExecution(); @@ -111,6 +114,23 @@ function BaseSelectionList({ const initialFocusedIndex = useMemo(() => data.findIndex((i) => i.keyForList === initiallyFocusedItemKey), [data, initiallyFocusedItemKey]); const [itemsToHighlight, setItemsToHighlight] = useState | null>(null); + const [announcedResultCount, setAnnouncedResultCount] = useState(''); + const announceTimeoutRef = useRef(null); + + // Debounced announcement of result count for screen readers + useEffect(() => { + if (!shouldShowTextInput) { + return; + } + + announceTimeoutRef.current = setTimeout(() => { + setAnnouncedResultCount(translate('common.resultsAvailable', data.length)); + }, CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME); + + return () => { + clearTimeout(announceTimeoutRef.current ?? undefined); + }; + }, [data.length, shouldShowTextInput, translate]); const isItemSelected = useCallback( (item: TItem) => item.isSelected ?? ((isSelected?.(item) ?? selectedItems.includes(item.keyForList)) && canSelectMultiple), @@ -545,6 +565,14 @@ function BaseSelectionList({ return ( {textInputComponent({shouldBeInsideList: false})} + {shouldShowTextInput && ( + + {announcedResultCount} + + )} {data.length === 0 && (shouldShowLoadingPlaceholder || shouldShowListEmptyContent) ? ( renderListEmptyContent() ) : ( diff --git a/src/components/ValidateCodeCountdown/index.tsx b/src/components/ValidateCodeCountdown/index.tsx index ea0435ac9c66..7dbf49faaa4e 100644 --- a/src/components/ValidateCodeCountdown/index.tsx +++ b/src/components/ValidateCodeCountdown/index.tsx @@ -1,5 +1,7 @@ import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {View} from 'react-native'; import RenderHTML from '@components/RenderHTML'; +import useAccessibilityAnnouncement from '@hooks/useAccessibilityAnnouncement'; import useLocalize from '@hooks/useLocalize'; import CONST from '@src/CONST'; import type {ValidateCodeCountdownProps} from './types'; @@ -27,12 +29,20 @@ function ValidateCodeCountdown({onCountdownFinish, ref}: ValidateCodeCountdownPr }; }, [onCountdownFinish, timeRemaining]); + // Announce countdown start/reset for screen readers + useAccessibilityAnnouncement(translate('validateCodeForm.timeRemainingAnnouncement', {timeRemaining: CONST.REQUEST_CODE_DELAY}), timeRemaining === CONST.REQUEST_CODE_DELAY); + + // Announce expiration for screen readers + useAccessibilityAnnouncement(translate('validateCodeForm.timeExpiredAnnouncement'), timeRemaining === 0); + return ( - + + + ); } diff --git a/src/languages/de.ts b/src/languages/de.ts index f84f0da1aba1..840ec056be59 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -283,7 +283,9 @@ const translations: TranslationDeepObject = { send: 'Senden', na: 'k. A.', noResultsFound: 'Keine Ergebnisse gefunden', - noResultsFoundMatching: (searchString: string) => `Keine Ergebnisse gefunden für „${searchString}“`, + noResultsFoundMatching: (searchString: string) => `Keine Ergebnisse gefunden für „${searchString}”`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'Ergebnis verfügbar' : 'Ergebnisse verfügbar'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'Vorschlag verfügbar' : 'Vorschläge verfügbar'}`, recentDestinations: 'Letzte Ziele', timePrefix: 'Es ist', conjunctionFor: 'für', @@ -2693,6 +2695,8 @@ ${amount} für ${merchant} – ${date}`, requiredWhen2FAEnabled: 'Erforderlich, wenn 2FA aktiviert ist', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Fordere einen neuen Code an in ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Neuen Code anfordern', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Verbleibende Zeit: ${timeRemaining} Sekunden`, + timeExpiredAnnouncement: 'Die Zeit ist abgelaufen', error: { pleaseFillMagicCode: 'Bitte gib deinen Magic Code ein', incorrectMagicCode: 'Falscher oder ungültiger Magic-Code. Bitte versuche es erneut oder fordere einen neuen Code an.', diff --git a/src/languages/en.ts b/src/languages/en.ts index c92d5bd00165..937f4e5d08cc 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -287,6 +287,8 @@ const translations = { na: 'N/A', noResultsFound: 'No results found', noResultsFoundMatching: (searchString: string) => `No results found matching "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'result' : 'results'} available`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestion' : 'suggestions'} available`, recentDestinations: 'Recent destinations', timePrefix: "It's", conjunctionFor: 'for', @@ -2728,6 +2730,8 @@ const translations = { requiredWhen2FAEnabled: 'Required when 2FA is enabled', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Request a new code in ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Request a new code', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Time remaining: ${timeRemaining} seconds`, + timeExpiredAnnouncement: 'The time has expired', error: { pleaseFillMagicCode: 'Please enter your magic code', incorrectMagicCode: 'Incorrect or invalid magic code. Please try again or request a new code.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1baa6afa156d..e80c834499a4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -157,6 +157,8 @@ const translations: TranslationDeepObject = { na: 'N/A', noResultsFound: 'No se han encontrado resultados', noResultsFoundMatching: (searchString: string) => `No se encontraron resultados que coincidan con "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultado disponible' : 'resultados disponibles'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugerencia disponible' : 'sugerencias disponibles'}`, recentDestinations: 'Destinos recientes', timePrefix: 'Son las', conjunctionFor: 'para', @@ -2556,6 +2558,8 @@ ${amount} para ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado', requestNewCode: ({timeRemaining}) => `Pedir un código nuevo en ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Solicitar un nuevo código', + timeRemainingAnnouncement: ({timeRemaining}) => `Tiempo restante: ${timeRemaining} segundos`, + timeExpiredAnnouncement: 'El tiempo ha expirado', error: { pleaseFillMagicCode: 'Por favor, introduce el código mágico.', incorrectMagicCode: 'Código mágico incorrecto o no válido. Inténtalo de nuevo o solicita otro código.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index b65c07543413..662dd3201ca3 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -284,6 +284,8 @@ const translations: TranslationDeepObject = { na: 'N/D', noResultsFound: 'Aucun résultat trouvé', noResultsFoundMatching: (searchString: string) => `Aucun résultat trouvé correspondant à « ${searchString} »`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'résultat disponible' : 'résultats disponibles'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestion disponible' : 'suggestions disponibles'}`, recentDestinations: 'Destinations récentes', timePrefix: "C'est", conjunctionFor: 'pour', @@ -2698,6 +2700,8 @@ ${amount} pour ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Obligatoire lorsque l’authentification à deux facteurs est activée', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Demander un nouveau code dans ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Demander un nouveau code', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Temps restant : ${timeRemaining} secondes`, + timeExpiredAnnouncement: 'Le temps est écoulé', error: { pleaseFillMagicCode: 'Veuillez saisir votre code magique', incorrectMagicCode: 'Code magique incorrect ou non valide. Veuillez réessayer ou demander un nouveau code.', diff --git a/src/languages/it.ts b/src/languages/it.ts index babe2339b9f6..9ca4684924c6 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -284,6 +284,8 @@ const translations: TranslationDeepObject = { na: 'N/D', noResultsFound: 'Nessun risultato trovato', noResultsFoundMatching: (searchString: string) => `Nessun risultato trovato per "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'risultato disponibile' : 'risultati disponibili'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggerimento disponibile' : 'suggerimenti disponibili'}`, recentDestinations: 'Destinazioni recenti', timePrefix: 'È', conjunctionFor: 'per', @@ -2687,6 +2689,8 @@ ${amount} per ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Obbligatorio quando l’autenticazione a due fattori è abilitata', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Richiedi un nuovo codice tra ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Richiedi un nuovo codice', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Tempo rimanente: ${timeRemaining} secondi`, + timeExpiredAnnouncement: 'Il tempo è scaduto', error: { pleaseFillMagicCode: 'Inserisci il tuo codice magico', incorrectMagicCode: 'Codice magico errato o non valido. Riprova o richiedi un nuovo codice.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b3a7c6691b6c..265d247c4cf1 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -284,6 +284,8 @@ const translations: TranslationDeepObject = { na: '該当なし', noResultsFound: '結果が見つかりません', noResultsFoundMatching: (searchString: string) => `"${searchString}" に一致する結果は見つかりませんでした`, + resultsAvailable: (count: number) => `${count}件の結果が利用可能`, + suggestionsAvailable: (count: number) => `${count}件の候補が利用可能`, recentDestinations: '最近の宛先', timePrefix: 'それは', conjunctionFor: '〜用', @@ -2667,6 +2669,8 @@ ${date} の ${merchant} への ${amount}`, requiredWhen2FAEnabled: '2要素認証が有効な場合は必須', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `${timeRemaining} 後に新しいコードをリクエストする`, requestNewCodeAfterErrorOccurred: '新しいコードをリクエスト', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `残り時間: ${timeRemaining}秒`, + timeExpiredAnnouncement: '時間切れです', error: { pleaseFillMagicCode: 'マジックコードを入力してください', incorrectMagicCode: '魔法コードが間違っているか無効です。もう一度お試しいただくか、新しいコードをリクエストしてください。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index be5b70b72ab6..a431300894ff 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -284,6 +284,8 @@ const translations: TranslationDeepObject = { na: 'n.v.t.', noResultsFound: 'Geen resultaten gevonden', noResultsFoundMatching: (searchString: string) => `Geen resultaten gevonden voor "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultaat beschikbaar' : 'resultaten beschikbaar'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'suggestie beschikbaar' : 'suggesties beschikbaar'}`, recentDestinations: 'Recente bestemmingen', timePrefix: 'Het is', conjunctionFor: 'voor', @@ -2686,6 +2688,8 @@ ${amount} voor ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Vereist wanneer 2FA is ingeschakeld', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Vraag een nieuwe code aan over ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Een nieuwe code aanvragen', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Resterende tijd: ${timeRemaining} seconden`, + timeExpiredAnnouncement: 'De tijd is verstreken', error: { pleaseFillMagicCode: 'Voer je magische code in', incorrectMagicCode: 'Onjuiste of ongeldige magische code. Probeer het opnieuw of vraag een nieuwe code aan.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 2fb31cbf66bd..22c192ffb0f8 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -284,6 +284,8 @@ const translations: TranslationDeepObject = { na: 'ND dotyczy', noResultsFound: 'Nie znaleziono wyników', noResultsFoundMatching: (searchString: string) => `Nie znaleziono wyników pasujących do „${searchString}”`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'wynik dostępny' : 'wyników dostępnych'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugestia dostępna' : 'sugestii dostępnych'}`, recentDestinations: 'Ostatnie miejsca docelowe', timePrefix: 'To jest', conjunctionFor: 'dla', @@ -2679,6 +2681,8 @@ ${amount} dla ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Wymagane, gdy włączone jest 2FA', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Poproś o nowy kod za ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Poproś o nowy kod', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Pozostały czas: ${timeRemaining} sekund`, + timeExpiredAnnouncement: 'Czas minął', error: { pleaseFillMagicCode: 'Wprowadź swój magiczny kod', incorrectMagicCode: 'Nieprawidłowy lub niepoprawny kod magiczny. Spróbuj ponownie lub poproś o nowy kod.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index eaf08826159e..41c545c77133 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -284,6 +284,8 @@ const translations: TranslationDeepObject = { na: 'N/D', noResultsFound: 'Nenhum resultado encontrado', noResultsFoundMatching: (searchString: string) => `Nenhum resultado encontrado para "${searchString}"`, + resultsAvailable: (count: number) => `${count} ${count === 1 ? 'resultado disponível' : 'resultados disponíveis'}`, + suggestionsAvailable: (count: number) => `${count} ${count === 1 ? 'sugestão disponível' : 'sugestões disponíveis'}`, recentDestinations: 'Destinos recentes', timePrefix: 'É', conjunctionFor: 'para', @@ -2679,6 +2681,8 @@ ${amount} para ${merchant} - ${date}`, requiredWhen2FAEnabled: 'Obrigatório quando a 2FA estiver ativada', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `Solicite um novo código em ${timeRemaining}`, requestNewCodeAfterErrorOccurred: 'Solicitar um novo código', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `Tempo restante: ${timeRemaining} segundos`, + timeExpiredAnnouncement: 'O tempo expirou', error: { pleaseFillMagicCode: 'Insira seu código mágico', incorrectMagicCode: 'Código mágico incorreto ou inválido. Tente novamente ou solicite um novo código.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index b4f23ecbec1c..96356c942827 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -283,7 +283,9 @@ const translations: TranslationDeepObject = { send: '发送', na: '不适用', noResultsFound: '未找到结果', - noResultsFoundMatching: (searchString: string) => `未找到与“${searchString}”匹配的结果`, + noResultsFoundMatching: (searchString: string) => `未找到与”${searchString}”匹配的结果`, + resultsAvailable: (count: number) => `${count}个结果可用`, + suggestionsAvailable: (count: number) => `${count}个建议可用`, recentDestinations: '最近目的地', timePrefix: '它是', conjunctionFor: '用于', @@ -2627,6 +2629,8 @@ ${amount},商户:${merchant} - 日期:${date}`, requiredWhen2FAEnabled: '启用双重验证时必填', requestNewCode: ({timeRemaining}: {timeRemaining: string}) => `在${timeRemaining}后请求新代码`, requestNewCodeAfterErrorOccurred: '请求新验证码', + timeRemainingAnnouncement: ({timeRemaining}: {timeRemaining: number}) => `剩余时间: ${timeRemaining}秒`, + timeExpiredAnnouncement: '时间已过期', error: { pleaseFillMagicCode: '请输入你的魔法验证码', incorrectMagicCode: '魔术验证码不正确或无效。请重试或请求新的验证码。', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 22eb7007262f..33c39a8c6690 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -13071,6 +13071,31 @@ function getReportFieldMaps(report: OnyxEntry, fieldList: Record, policy: OnyxEntry, reportViolations?: OnyxEntry): boolean { + if (!report || !policy?.fieldList || !policy?.areReportFieldsEnabled) { + return false; + } + + if (!isPaidGroupPolicyExpenseReport(report) && !isInvoiceReport(report)) { + return false; + } + + const {fieldsByName} = getReportFieldMaps(report, policy.fieldList); + + return Object.values(fieldsByName).some((field) => { + if (field.target !== report.type) { + return false; + } + if (shouldHideSingleReportField(field)) { + return false; + } + if (isReportFieldDisabledForUser(report, field, policy)) { + return false; + } + return !!getFieldViolation(reportViolations, field); + }); +} + function getLinkedIOUTransaction(reportAction: OnyxEntry, transactions: Transaction[]): OnyxEntry { const transactionID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUTransactionID : undefined; return transactionID ? transactions.find((item) => item.transactionID === transactionID) : undefined; @@ -13262,6 +13287,7 @@ export { hasSmartscanError, hasUpdatedTotal, hasViolations, + hasVisibleReportFieldViolations, hasWarningTypeViolations, hasNoticeTypeViolations, hasAnyViolations, diff --git a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts index 2360aff7e04b..1cf8d7b402f9 100644 --- a/src/libs/actions/OnyxDerived/configs/reportAttributes.ts +++ b/src/libs/actions/OnyxDerived/configs/reportAttributes.ts @@ -1,6 +1,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {computeReportName} from '@libs/ReportNameUtils'; -import {generateIsEmptyReport, generateReportAttributes, isArchivedReport, isValidReport} from '@libs/ReportUtils'; +import {generateIsEmptyReport, generateReportAttributes, hasVisibleReportFieldViolations, isArchivedReport, isValidReport} from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; import {hasKeyTriggeredCompute} from '@userActions/OnyxDerived/utils'; @@ -20,7 +20,8 @@ const prepareReportKeys = (keys: string[]) => { key .replace(ONYXKEYS.COLLECTION.REPORT_METADATA, ONYXKEYS.COLLECTION.REPORT) .replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT) - .replace(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, ONYXKEYS.COLLECTION.REPORT), + .replace(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, ONYXKEYS.COLLECTION.REPORT) + .replace(ONYXKEYS.COLLECTION.REPORT_VIOLATIONS, ONYXKEYS.COLLECTION.REPORT), ), ), ]; @@ -72,10 +73,11 @@ export default createOnyxDerivedValueConfig({ ONYXKEYS.SESSION, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.COLLECTION.POLICY_TAGS, + ONYXKEYS.COLLECTION.REPORT_VIOLATIONS, ONYXKEYS.COLLECTION.REPORT_METADATA, ], compute: ( - [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies, policyTags], + [reports, preferredLocale, transactionViolations, reportActions, reportNameValuePairs, transactions, personalDetails, session, policies, policyTags, reportViolations], {currentValue, sourceValues}, ) => { // Check if display names changed when personal details are updated @@ -114,6 +116,7 @@ export default createOnyxDerivedValueConfig({ const reportMetadataUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_METADATA] ?? {}; const reportActionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_ACTIONS] ?? {}; const reportNameValuePairsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS] ?? {}; + const reportViolationsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_VIOLATIONS] ?? {}; const transactionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.TRANSACTION]; const transactionViolationsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]; let dataToIterate = Object.keys(reports); @@ -138,6 +141,7 @@ export default createOnyxDerivedValueConfig({ ...Object.keys(reportMetadataUpdates), ...Object.keys(reportActionsUpdates), ...Object.keys(reportNameValuePairsUpdates), + ...Object.keys(reportViolationsUpdates), ...Array.from(reportUpdatesRelatedToReportActions), ]; @@ -217,10 +221,24 @@ export default createOnyxDerivedValueConfig({ isReportArchived, }); + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`]; + const hasFieldViolations = hasVisibleReportFieldViolations(report, policy, reportViolations?.[`${ONYXKEYS.COLLECTION.REPORT_VIOLATIONS}${report.reportID}`]); + let brickRoadStatus; let actionBadge; // if report has errors or violations, show red dot - if (SidebarUtils.shouldShowRedBrickRoad(report, chatReport, reportActionsList, hasAnyViolations, reportErrors, transactions, transactionViolations, !!isReportArchived)) { + if ( + SidebarUtils.shouldShowRedBrickRoad( + report, + chatReport, + reportActionsList, + hasAnyViolations || hasFieldViolations, + reportErrors, + transactions, + transactionViolations, + !!isReportArchived, + ) + ) { brickRoadStatus = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; actionBadge = CONST.REPORT.ACTION_BADGE.FIX; } diff --git a/tests/unit/OnyxDerivedTest.tsx b/tests/unit/OnyxDerivedTest.tsx index d12448d36d7c..028ee038fd3b 100644 --- a/tests/unit/OnyxDerivedTest.tsx +++ b/tests/unit/OnyxDerivedTest.tsx @@ -123,9 +123,9 @@ describe('OnyxDerived', () => { const transaction = createRandomTransaction(1); // When the report attributes are recomputed with both report and transaction updates - reportAttributes.compute([reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], {}); + reportAttributes.compute([reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], {}); const reportAttributesComputedValue = reportAttributes.compute( - [reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], + [reports, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], { sourceValues: { [ONYXKEYS.COLLECTION.REPORT]: {