diff --git a/src/CONST.ts b/src/CONST.ts index 268657bf35b3..ed5f1837fe3b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6299,10 +6299,17 @@ const CONST = { }, DEBUG: { + FORMS: { + REPORT: 'report', + REPORT_ACTION: 'reportAction', + TRANSACTION: 'transaction', + TRANSACTION_VIOLATION: 'transactionViolation', + }, DETAILS: 'details', JSON: 'json', REPORT_ACTIONS: 'actions', REPORT_ACTION_PREVIEW: 'preview', + TRANSACTION_VIOLATIONS: 'violations', }, REPORT_IN_LHN_REASONS: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b4510a2faeed..f97edbd744eb 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -716,10 +716,6 @@ const ONYXKEYS = { RULES_MAX_EXPENSE_AMOUNT_FORM_DRAFT: 'rulesMaxExpenseAmountFormDraft', RULES_MAX_EXPENSE_AGE_FORM: 'rulesMaxExpenseAgeForm', RULES_MAX_EXPENSE_AGE_FORM_DRAFT: 'rulesMaxExpenseAgeFormDraft', - DEBUG_REPORT_PAGE_FORM: 'debugReportPageForm', - DEBUG_REPORT_PAGE_FORM_DRAFT: 'debugReportPageFormDraft', - DEBUG_REPORT_ACTION_PAGE_FORM: 'debugReportActionPageForm', - DEBUG_REPORT_ACTION_PAGE_FORM_DRAFT: 'debugReportActionPageFormDraft', DEBUG_DETAILS_FORM: 'debugDetailsForm', DEBUG_DETAILS_FORM_DRAFT: 'debugDetailsFormDraft', }, @@ -814,9 +810,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AMOUNT_FORM]: FormTypes.RulesMaxExpenseAmountForm; [ONYXKEYS.FORMS.RULES_MAX_EXPENSE_AGE_FORM]: FormTypes.RulesMaxExpenseAgeForm; [ONYXKEYS.FORMS.SEARCH_SAVED_SEARCH_RENAME_FORM]: FormTypes.SearchSavedSearchRenameForm; - [ONYXKEYS.FORMS.DEBUG_REPORT_PAGE_FORM]: FormTypes.DebugReportForm; - [ONYXKEYS.FORMS.DEBUG_REPORT_ACTION_PAGE_FORM]: FormTypes.DebugReportActionForm; - [ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm; + [ONYXKEYS.FORMS.DEBUG_DETAILS_FORM]: FormTypes.DebugReportForm | FormTypes.DebugReportActionForm | FormTypes.DebugTransactionForm | FormTypes.DebugTransactionViolationForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bdf4d4774ec1..2c44551acaa7 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1777,13 +1777,46 @@ const ROUTES = { getRoute: (reportID: string, reportActionID: string) => `debug/report/${reportID}/actions/${reportActionID}/preview` as const, }, DETAILS_CONSTANT_PICKER_PAGE: { - route: 'debug/details/constant/:fieldName', - getRoute: (fieldName: string, fieldValue?: string, backTo?: string) => getUrlWithBackToParam(`debug/details/constant/${fieldName}?fieldValue=${fieldValue}`, backTo), + route: 'debug/:formType/details/constant/:fieldName', + getRoute: (formType: string, fieldName: string, fieldValue?: string, policyID?: string, backTo?: string) => + getUrlWithBackToParam(`debug/${formType}/details/constant/${fieldName}?fieldValue=${fieldValue}&policyID=${policyID}`, backTo), }, DETAILS_DATE_TIME_PICKER_PAGE: { route: 'debug/details/datetime/:fieldName', getRoute: (fieldName: string, fieldValue?: string, backTo?: string) => getUrlWithBackToParam(`debug/details/datetime/${fieldName}?fieldValue=${fieldValue}`, backTo), }, + DEBUG_TRANSACTION: { + route: 'debug/transaction/:transactionID', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}` as const, + }, + DEBUG_TRANSACTION_TAB_DETAILS: { + route: 'debug/transaction/:transactionID/details', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/details` as const, + }, + DEBUG_TRANSACTION_TAB_JSON: { + route: 'debug/transaction/:transactionID/json', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/json` as const, + }, + DEBUG_TRANSACTION_TAB_VIOLATIONS: { + route: 'debug/transaction/:transactionID/violations', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/violations` as const, + }, + DEBUG_TRANSACTION_VIOLATION_CREATE: { + route: 'debug/transaction/:transactionID/violations/create', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/violations/create` as const, + }, + DEBUG_TRANSACTION_VIOLATION: { + route: 'debug/transaction/:transactionID/violations/:index', + getRoute: (transactionID: string, index: string) => `debug/transaction/${transactionID}/violations/${index}` as const, + }, + DEBUG_TRANSACTION_VIOLATION_TAB_DETAILS: { + route: 'debug/transaction/:transactionID/violations/:index/details', + getRoute: (transactionID: string, index: string) => `debug/transaction/${transactionID}/violations/${index}/details` as const, + }, + DEBUG_TRANSACTION_VIOLATION_TAB_JSON: { + route: 'debug/transaction/:transactionID/violations/:index/json', + getRoute: (transactionID: string, index: string) => `debug/transaction/${transactionID}/violations/${index}/json` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 543e8708fea3..536dddb1f637 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -620,6 +620,9 @@ const SCREENS = { REPORT_ACTION_CREATE: 'Debug_Report_Action_Create', DETAILS_CONSTANT_PICKER_PAGE: 'Debug_Details_Constant_Picker_Page', DETAILS_DATE_TIME_PICKER_PAGE: 'Debug_Details_Date_Time_Picker_Page', + TRANSACTION: 'Debug_Transaction', + TRANSACTION_VIOLATION_CREATE: 'Debug_Transaction_Violation_Create', + TRANSACTION_VIOLATION: 'Debug_Transaction_Violation', }, } as const; diff --git a/src/components/CurrencySelectionList/index.tsx b/src/components/CurrencySelectionList/index.tsx index 201ed7bab730..1e8b5294286f 100644 --- a/src/components/CurrencySelectionList/index.tsx +++ b/src/components/CurrencySelectionList/index.tsx @@ -1,6 +1,6 @@ import {Str} from 'expensify-common'; import React, {useCallback, useMemo, useState} from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import SelectableListItem from '@components/SelectionList/SelectableListItem'; @@ -8,17 +8,17 @@ import useLocalize from '@hooks/useLocalize'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {CurrencyListItem, CurrencySelectionListOnyxProps, CurrencySelectionListProps} from './types'; +import type {CurrencyListItem, CurrencySelectionListProps} from './types'; function CurrencySelectionList({ searchInputLabel, initiallySelectedCurrencyCode, onSelect, - currencyList, selectedCurrencies = [], canSelectMultiple = false, recentlyUsedCurrencies, }: CurrencySelectionListProps) { + const [currencyList] = useOnyx(ONYXKEYS.CURRENCY_LIST); const [searchValue, setSearchValue] = useState(''); const {translate} = useLocalize(); const getUnselectedOptions = useCallback((options: CurrencyListItem[]) => options.filter((option) => !option.isSelected), []); @@ -107,8 +107,4 @@ function CurrencySelectionList({ CurrencySelectionList.displayName = 'CurrencySelectionList'; -const CurrencySelectionListWithOnyx = withOnyx({ - currencyList: {key: ONYXKEYS.CURRENCY_LIST}, -})(CurrencySelectionList); - -export default CurrencySelectionListWithOnyx; +export default CurrencySelectionList; diff --git a/src/components/CurrencySelectionList/types.ts b/src/components/CurrencySelectionList/types.ts index 3001b0ceeaab..5cfef604ab94 100644 --- a/src/components/CurrencySelectionList/types.ts +++ b/src/components/CurrencySelectionList/types.ts @@ -1,18 +1,11 @@ -import type {OnyxEntry} from 'react-native-onyx'; import type {ListItem} from '@components/SelectionList/types'; -import type {CurrencyList} from '@src/types/onyx'; type CurrencyListItem = ListItem & { currencyName: string; currencyCode: string; }; -type CurrencySelectionListOnyxProps = { - /** List of available currencies */ - currencyList: OnyxEntry; -}; - -type CurrencySelectionListProps = CurrencySelectionListOnyxProps & { +type CurrencySelectionListProps = { /** Label for the search text input */ searchInputLabel: string; @@ -32,4 +25,4 @@ type CurrencySelectionListProps = CurrencySelectionListOnyxProps & { canSelectMultiple?: boolean; }; -export type {CurrencyListItem, CurrencySelectionListProps, CurrencySelectionListOnyxProps}; +export type {CurrencyListItem, CurrencySelectionListProps}; diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index dfc88840446f..de20575aeef4 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -122,7 +122,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo return ( policyTag.enabled && !selectedNames.includes(policyTag.name))]; }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); - const sections = useMemo( - () => - TagOptionListUtils.getTagListSections({ - searchValue, - selectedOptions, - tags: enabledTags, - recentlyUsedTags: policyRecentlyUsedTagsList, - }), - [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], - ); + const sections = useMemo(() => { + const tagSections = TagOptionListUtils.getTagListSections({ + searchValue, + selectedOptions, + tags: enabledTags, + recentlyUsedTags: policyRecentlyUsedTagsList, + }); + return shouldOrderListByTagName + ? tagSections.map((option) => ({ + ...option, + data: option.data.sort((a, b) => a.text?.localeCompare(b.text ?? '') ?? 0), + })) + : tagSections; + }, [searchValue, selectedOptions, enabledTags, policyRecentlyUsedTagsList, shouldOrderListByTagName]); const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList((sections?.at(0)?.data?.length ?? 0) > 0, searchValue); diff --git a/src/languages/en.ts b/src/languages/en.ts index b1273cec4dab..9bea1261ddbd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5328,6 +5328,9 @@ const translations = { createReportAction: 'Create Report Action', reportAction: 'Report Action', report: 'Report', + transaction: 'Transaction', + violations: 'Violations', + transactionViolation: 'Transaction Violation', hint: "Data changes won't be sent to the backend", textFields: 'Text fields', numberFields: 'Number fields', @@ -5343,6 +5346,8 @@ const translations = { true: 'true', false: 'false', viewReport: 'View Report', + viewTransaction: 'View transaction', + createTransactionViolation: 'Create transaction violation', reasonVisibleInLHN: { hasDraftComment: 'Has draft comment', hasGBR: 'Has GBR', diff --git a/src/languages/es.ts b/src/languages/es.ts index 7c3a99694b0e..7e6f8efc897a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5847,6 +5847,9 @@ const translations = { createReportAction: 'Crear Report Action', reportAction: 'Report Action', report: 'Report', + transaction: 'Transacción', + violations: 'Violaciones', + transactionViolation: 'Violación de transacción', hint: 'Los cambios de datos no se enviarán al backend', textFields: 'Campos de texto', numberFields: 'Campos numéricos', @@ -5862,6 +5865,8 @@ const translations = { true: 'verdadero', false: 'falso', viewReport: 'Ver Informe', + viewTransaction: 'Ver transacción', + createTransactionViolation: 'Crear infracción de transacción', reasonVisibleInLHN: { hasDraftComment: 'Tiene comentario en borrador', hasGBR: 'Tiene GBR', diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index eef25d02ef0a..671fb03f268b 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1,13 +1,18 @@ +/* eslint-disable default-case */ + /* eslint-disable max-classes-per-file */ import {isMatch, isValid} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type {TupleToUnion} from 'type-fest'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Policy, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type {Beta, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; +import * as ReportActionsUtils from './ReportActionsUtils'; import * as ReportUtils from './ReportUtils'; import SidebarUtils from './SidebarUtils'; +import * as TransactionUtils from './TransactionUtils'; class NumberError extends SyntaxError { constructor() { @@ -35,38 +40,25 @@ class ObjectError extends SyntaxError { } } -type ObjectType = Record; +type ObjectType> = Record; -type ConstantEnum = Record; +type ConstantEnum = Record>; type PropertyTypes = Array<'string' | 'number' | 'object' | 'boolean' | 'undefined'>; -const OPTIONAL_BOOLEAN_STRINGS = ['true', 'false', 'undefined']; +type ArrayTypeFromOnyxDefinition = T extends unknown[] ? NonNullable : never; + +type ArrayElement, K extends keyof TOnyx> = ArrayTypeFromOnyxDefinition[K]>; -const REPORT_NUMBER_PROPERTIES: Array = [ - 'lastReadSequenceNumber', - 'managerID', - 'lastActorAccountID', - 'ownerAccountID', - 'total', - 'unheldTotal', - 'nonReimbursableTotal', -] satisfies Array; - -const REPORT_BOOLEAN_PROPERTIES: Array = [ - 'hasOutstandingChildRequest', - 'hasOutstandingChildTask', - 'isOwnPolicyExpenseChat', - 'isPinned', - 'hasParentAccess', - 'isDeletedParentAction', - 'isOptimisticReport', - 'isWaitingOnBankAccount', - 'isCancelledIOU', - 'isHidden', -] satisfies Array; - -const REPORT_DATE_PROPERTIES: Array = ['lastVisibleActionCreated', 'lastReadTime', 'lastMentionedTime', 'lastVisibleActionLastModified'] satisfies Array; +type KeysOfUnion = T extends T ? keyof T : never; + +type ObjectElement = Required[K] extends Record + ? TCollectionKey extends string | number + ? {[ValueTypeKey in KeysOfUnion]: ValueType[ValueTypeKey]} + : {[ElementKey in KeysOfUnion[K]>]: Required[K]>[ElementKey]} + : never; + +const OPTIONAL_BOOLEAN_STRINGS = ['true', 'false', 'undefined']; const REPORT_REQUIRED_PROPERTIES: Array = ['reportID'] satisfies Array; @@ -88,18 +80,9 @@ const REPORT_ACTION_NUMBER_PROPERTIES: Array = [ 'timestamp', ] satisfies Array; -const REPORT_ACTION_BOOLEAN_PROPERTIES: Array = [ - 'isLoading', - 'automatic', - 'shouldShow', - 'isFirstItem', - 'isAttachmentOnly', - 'isAttachmentWithText', - 'isNewestReportAction', - 'isOptimisticAction', -] satisfies Array; +const TRANSACTION_REQUIRED_PROPERTIES: Array = ['transactionID', 'reportID', 'amount', 'created', 'currency', 'merchant'] satisfies Array; -const REPORT_ACTION_DATE_PROPERTIES: Array = ['created', 'lastModified'] satisfies Array; +const TRANSACTION_VIOLATION_REQUIRED_PROPERTIES: Array = ['type', 'name'] satisfies Array; let isInFocusMode: OnyxEntry; Onyx.connect({ @@ -174,6 +157,10 @@ type OnyxData = (T extends 'number' ? number : T extends * @throws {SyntaxError} if type is object but the provided string does not represent an object */ function stringToOnyxData(data: string, type?: T): OnyxData { + if (isEmptyValue(data)) { + return data as OnyxData; + } + let onyxData; switch (type) { @@ -235,11 +222,28 @@ function onyxDataToDraftData(data: OnyxEntry>) { return Object.fromEntries(Object.entries(data ?? {}).map(([key, value]) => [key, onyxDataToString(value)])); } +/** + * Whether a string representation is an empty value + * + * @param value - string representantion + * @returns whether the value is an empty value + */ +function isEmptyValue(value: string): boolean { + switch (value) { + case 'undefined': + case 'null': + case '': + return true; + default: + return false; + } +} + /** * Validates if a string is a valid representation of a number. */ function validateNumber(value: string) { - if (value === 'undefined' || value === '' || (!value.includes(' ') && !Number.isNaN(Number(value)))) { + if (isEmptyValue(value) || (!value.includes(' ') && !Number.isNaN(Number(value)))) { return; } @@ -261,7 +265,7 @@ function validateBoolean(value: string) { * Validates if a string is a valid representation of a date. */ function validateDate(value: string) { - if (value === 'undefined' || (isMatch(value, CONST.DATE.FNS_DB_FORMAT_STRING) && isValid(new Date(value)))) { + if (isEmptyValue(value) || ((isMatch(value, CONST.DATE.FNS_DB_FORMAT_STRING) || isMatch(value, CONST.DATE.FNS_FORMAT_STRING)) && isValid(new Date(value)))) { return; } @@ -279,7 +283,7 @@ function validateConstantEnum(value: string, constEnum: ConstantEnum) { return String(val); }); - if (value === 'undefined' || value === '' || enumValues.includes(value)) { + if (isEmptyValue(value) || enumValues.includes(value)) { return; } @@ -289,11 +293,15 @@ function validateConstantEnum(value: string, constEnum: ConstantEnum) { /** * Validates if a string is a valid representation of an array. */ -function validateArray( +function validateArray | 'constantEnum' = 'string'>( value: string, - arrayType: 'string' | 'number' | 'boolean' | ConstantEnum | Record, + arrayType: T extends Record + ? Record + : T extends 'constantEnum' + ? ConstantEnum + : T, ) { - if (value === 'undefined') { + if (isEmptyValue(value)) { return; } @@ -306,22 +314,22 @@ function validateArray( array.forEach((element) => { // Element is an object if (element && typeof element === 'object' && typeof arrayType === 'object') { - Object.entries(arrayType).forEach(([key, val]) => { - const property = element[key as keyof typeof element]; + Object.entries(element).forEach(([key, val]) => { + const expectedType = arrayType[key as keyof typeof arrayType]; // Property is a constant enum, so we apply validateConstantEnum - if (typeof val === 'object' && !Array.isArray(val)) { - return validateConstantEnum(property, val as ConstantEnum); + if (typeof expectedType === 'object' && !Array.isArray(expectedType)) { + return validateConstantEnum(String(val), expectedType as ConstantEnum); } // Expected property type is array - if (val === 'array') { + if (expectedType === 'array') { // Property type is not array - if (!Array.isArray(property)) { + if (!Array.isArray(val)) { throw new ArrayError(arrayType); } return; } // Property type is not one of the valid types - if (Array.isArray(val) ? !val.includes(typeof property) : typeof property !== val) { + if (Array.isArray(expectedType) ? !expectedType.includes(typeof val as TupleToUnion) : typeof val !== expectedType) { throw new ArrayError(arrayType); } }); @@ -345,8 +353,8 @@ function validateArray( /** * Validates if a string is a valid representation of an object. */ -function validateObject(value: string, type: ObjectType, collectionIndexType?: 'string' | 'number') { - if (value === 'undefined') { +function validateObject>(value: string, type: ObjectType, collectionIndexType?: 'string' | 'number') { + if (isEmptyValue(value)) { return; } @@ -356,7 +364,7 @@ function validateObject(value: string, type: ObjectType, collectionIndexType?: ' } : type; - const object = parseJSON(value) as ObjectType; + const object = parseJSON(value) as ObjectType; if (typeof object !== 'object' || Array.isArray(object)) { throw new ObjectError(expectedType); @@ -381,12 +389,13 @@ function validateObject(value: string, type: ObjectType, collectionIndexType?: ' throw new ObjectError(expectedType); } - Object.entries(type).forEach(([key, val]) => { - // test[key] is a constant enum - if (typeof val === 'object') { - return validateConstantEnum(test[key] as string, val); + Object.entries(test).forEach(([key, val]) => { + const expectedValueType = type[key]; + // val is a constant enum + if (typeof expectedValueType === 'object') { + return validateConstantEnum(val as string, expectedValueType); } - if (val === 'array' ? !Array.isArray(test[key]) : typeof test[key] !== val) { + if (expectedValueType === 'array' ? !Array.isArray(val) : typeof val !== expectedValueType) { throw new ObjectError(expectedType); } }); @@ -397,7 +406,7 @@ function validateObject(value: string, type: ObjectType, collectionIndexType?: ' * Validates if a string is a valid representation of a string. */ function validateString(value: string) { - if (value === 'undefined') { + if (isEmptyValue(value)) { return; } @@ -415,6 +424,17 @@ function validateString(value: string) { } } +/** + * Execute validation of a union type (e.g. Record | Array) + */ +function unionValidation(firstValidation: () => void, secondValidation: () => void) { + try { + firstValidation(); + } catch (e) { + secondValidation(); + } +} + /** * Validates if a property of Report is of the expected type * @@ -422,78 +442,200 @@ function validateString(value: string) { * @param value - value provided by the user */ function validateReportDraftProperty(key: keyof Report, value: string) { - if (REPORT_REQUIRED_PROPERTIES.includes(key) && value === 'undefined') { + if (REPORT_REQUIRED_PROPERTIES.includes(key) && isEmptyValue(value)) { throw SyntaxError('debug.missingValue'); } - if (key === 'privateNotes') { - return validateObject( - value, - { - note: 'string', - }, - 'number', - ); - } - if (key === 'permissions') { - return validateArray(value, CONST.REPORT.PERMISSIONS); - } - if (key === 'pendingChatMembers') { - return validateArray(value, { - accountID: 'string', - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, - }); - } - if (key === 'participants') { - return validateObject( - value, - { - notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE, - }, - 'number', - ); - } - if (REPORT_NUMBER_PROPERTIES.includes(key)) { - return validateNumber(value); - } - if (REPORT_BOOLEAN_PROPERTIES.includes(key)) { - return validateBoolean(value); - } - if (REPORT_DATE_PROPERTIES.includes(key)) { - return validateDate(value); - } - if (key === 'tripData') { - return validateObject(value, { - startDate: 'string', - endDate: 'string', - tripID: 'string', - }); - } - if (key === 'lastActionType') { - return validateConstantEnum(value, CONST.REPORT.ACTIONS.TYPE); - } - if (key === 'writeCapability') { - return validateConstantEnum(value, CONST.REPORT.WRITE_CAPABILITIES); - } - if (key === 'visibility') { - return validateConstantEnum(value, CONST.REPORT.VISIBILITY); - } - if (key === 'stateNum') { - return validateConstantEnum(value, CONST.REPORT.STATE_NUM); - } - if (key === 'statusNum') { - return validateConstantEnum(value, CONST.REPORT.STATUS_NUM); - } - if (key === 'chatType') { - return validateConstantEnum(value, CONST.REPORT.CHAT_TYPE); - } - if (key === 'errorFields') { - return validateObject(value, {}); - } - if (key === 'pendingFields') { - return validateObject(value, {}); + switch (key) { + case 'avatarUrl': + case 'lastMessageText': + case 'lastVisibleActionCreated': + case 'lastReadTime': + case 'lastMentionedTime': + case 'policyAvatar': + case 'policyName': + case 'oldPolicyName': + case 'description': + case 'policyID': + case 'reportName': + case 'reportID': + case 'reportActionID': + case 'chatReportID': + case 'type': + case 'lastMessageTranslationKey': + case 'parentReportID': + case 'parentReportActionID': + case 'lastVisibleActionLastModified': + case 'lastMessageHtml': + case 'currency': + case 'iouReportID': + case 'preexistingReportID': + case 'private_isArchived': + return validateString(value); + case 'hasOutstandingChildRequest': + case 'hasOutstandingChildTask': + case 'isOwnPolicyExpenseChat': + case 'isPinned': + case 'hasParentAccess': + case 'isDeletedParentAction': + case 'isOptimisticReport': + case 'isWaitingOnBankAccount': + case 'isCancelledIOU': + case 'isHidden': + return validateBoolean(value); + case 'lastReadSequenceNumber': + case 'managerID': + case 'lastActorAccountID': + case 'ownerAccountID': + case 'total': + case 'unheldTotal': + case 'nonReimbursableTotal': + return validateNumber(value); + case 'chatType': + return validateConstantEnum(value, CONST.REPORT.CHAT_TYPE); + case 'stateNum': + return validateConstantEnum(value, CONST.REPORT.STATE_NUM); + case 'statusNum': + return validateConstantEnum(value, CONST.REPORT.STATUS_NUM); + case 'writeCapability': + return validateConstantEnum(value, CONST.REPORT.WRITE_CAPABILITIES); + case 'visibility': + return validateConstantEnum(value, CONST.REPORT.VISIBILITY); + case 'invoiceReceiver': + return validateObject>(value, { + type: 'string', + policyID: 'string', + accountID: 'string', + }); + case 'lastActionType': + return validateConstantEnum(value, CONST.REPORT.ACTIONS.TYPE); + case 'participants': + return validateObject>( + value, + { + role: CONST.REPORT.ROLE, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + pendingFields: 'object', + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE, + }, + 'number', + ); + case 'errorFields': + return validateObject>(value, {}, 'string'); + case 'privateNotes': + return validateObject>( + value, + { + note: 'string', + errors: 'string', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + pendingFields: 'object', + }, + 'number', + ); + case 'pendingChatMembers': + return validateArray>(value, { + accountID: 'string', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + errors: 'object', + }); + case 'fieldList': + return validateObject>( + value, + { + fieldID: 'string', + type: 'string', + name: 'string', + keys: 'array', + values: 'array', + defaultValue: 'string', + orderWeight: 'number', + deletable: 'boolean', + value: 'string', + target: 'string', + externalIDs: 'array', + disabledOptions: 'array', + isTax: 'boolean', + externalID: 'string', + origin: 'string', + defaultExternalID: 'string', + }, + 'string', + ); + case 'permissions': + return validateArray<'constantEnum'>(value, CONST.REPORT.PERMISSIONS); + case 'tripData': + return validateObject>(value, { + startDate: 'string', + endDate: 'string', + tripID: 'string', + }); + case 'pendingAction': + return validateConstantEnum(value, CONST.RED_BRICK_ROAD_PENDING_ACTION); + case 'pendingFields': + return validateObject>(value, { + description: CONST.RED_BRICK_ROAD_PENDING_ACTION, + privateNotes: CONST.RED_BRICK_ROAD_PENDING_ACTION, + currency: CONST.RED_BRICK_ROAD_PENDING_ACTION, + type: CONST.RED_BRICK_ROAD_PENDING_ACTION, + policyID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + reportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + avatarUrl: CONST.RED_BRICK_ROAD_PENDING_ACTION, + chatType: CONST.RED_BRICK_ROAD_PENDING_ACTION, + hasOutstandingChildRequest: CONST.RED_BRICK_ROAD_PENDING_ACTION, + hasOutstandingChildTask: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isOwnPolicyExpenseChat: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isPinned: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastMessageText: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastVisibleActionCreated: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastReadTime: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastReadSequenceNumber: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastMentionedTime: CONST.RED_BRICK_ROAD_PENDING_ACTION, + policyAvatar: CONST.RED_BRICK_ROAD_PENDING_ACTION, + policyName: CONST.RED_BRICK_ROAD_PENDING_ACTION, + oldPolicyName: CONST.RED_BRICK_ROAD_PENDING_ACTION, + hasParentAccess: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isDeletedParentAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION, + reportActionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + chatReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + stateNum: CONST.RED_BRICK_ROAD_PENDING_ACTION, + statusNum: CONST.RED_BRICK_ROAD_PENDING_ACTION, + writeCapability: CONST.RED_BRICK_ROAD_PENDING_ACTION, + visibility: CONST.RED_BRICK_ROAD_PENDING_ACTION, + invoiceReceiver: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastMessageTranslationKey: CONST.RED_BRICK_ROAD_PENDING_ACTION, + parentReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + parentReportActionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isOptimisticReport: CONST.RED_BRICK_ROAD_PENDING_ACTION, + managerID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastVisibleActionLastModified: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastMessageHtml: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastActorAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastActionType: CONST.RED_BRICK_ROAD_PENDING_ACTION, + ownerAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + participants: CONST.RED_BRICK_ROAD_PENDING_ACTION, + total: CONST.RED_BRICK_ROAD_PENDING_ACTION, + unheldTotal: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isWaitingOnBankAccount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isCancelledIOU: CONST.RED_BRICK_ROAD_PENDING_ACTION, + iouReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + preexistingReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + nonReimbursableTotal: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isHidden: CONST.RED_BRICK_ROAD_PENDING_ACTION, + pendingChatMembers: CONST.RED_BRICK_ROAD_PENDING_ACTION, + fieldList: CONST.RED_BRICK_ROAD_PENDING_ACTION, + permissions: CONST.RED_BRICK_ROAD_PENDING_ACTION, + tripData: CONST.RED_BRICK_ROAD_PENDING_ACTION, + // eslint-disable-next-line @typescript-eslint/naming-convention + private_isArchived: CONST.RED_BRICK_ROAD_PENDING_ACTION, + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION, + avatar: CONST.RED_BRICK_ROAD_PENDING_ACTION, + createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION, + partial: CONST.RED_BRICK_ROAD_PENDING_ACTION, + reimbursed: CONST.RED_BRICK_ROAD_PENDING_ACTION, + preview: CONST.RED_BRICK_ROAD_PENDING_ACTION, + }); } - - validateString(value); } /** @@ -503,49 +645,608 @@ function validateReportDraftProperty(key: keyof Report, value: string) { * @param value - value provided by the user */ function validateReportActionDraftProperty(key: keyof ReportAction, value: string) { - if (REPORT_ACTION_REQUIRED_PROPERTIES.includes(key) && value === 'undefined') { + if (REPORT_ACTION_REQUIRED_PROPERTIES.includes(key) && isEmptyValue(value)) { throw SyntaxError('debug.missingValue'); } - if (REPORT_ACTION_NUMBER_PROPERTIES.includes(key)) { - return validateNumber(value); - } - if (REPORT_ACTION_BOOLEAN_PROPERTIES.includes(key)) { - return validateBoolean(value); - } - if (key === 'actionName') { - return validateConstantEnum(value, CONST.REPORT.ACTIONS.TYPE); - } - if (key === 'childStatusNum') { - return validateConstantEnum(value, CONST.REPORT.STATUS_NUM); - } - if (key === 'childStateNum') { - return validateConstantEnum(value, CONST.REPORT.STATE_NUM); - } - if (key === 'childReportNotificationPreference') { - return validateConstantEnum(value, CONST.REPORT.NOTIFICATION_PREFERENCE); - } - if (REPORT_ACTION_DATE_PROPERTIES.includes(key)) { - return validateDate(value); - } - if (key === 'whisperedToAccountIDs') { - return validateArray(value, 'number'); - } - if (key === 'message') { - return validateArray(value, {text: 'string', html: ['string', 'undefined'], type: 'string'}); + switch (key) { + case 'reportID': + case 'reportActionID': + case 'parentReportID': + case 'childReportID': + case 'childReportName': + case 'childType': + case 'childOldestFourAccountIDs': + case 'childLastVisibleActionCreated': + case 'actor': + case 'avatar': + case 'childLastMoneyRequestComment': + case 'reportActionTimestamp': + case 'timestamp': + case 'error': + return validateString(value); + case 'actorAccountID': + case 'sequenceNumber': + case 'accountID': + case 'childCommenterCount': + case 'childVisibleActionCount': + case 'childManagerAccountID': + case 'childOwnerAccountID': + case 'childLastActorAccountID': + case 'childMoneyRequestCount': + case 'adminAccountID': + case 'delegateAccountID': + return validateNumber(value); + case 'isLoading': + case 'automatic': + case 'shouldShow': + case 'isFirstItem': + case 'isAttachmentOnly': + case 'isAttachmentWithText': + case 'isNewestReportAction': + case 'isOptimisticAction': + return validateBoolean(value); + case 'created': + case 'lastModified': + return validateDate(value); + case 'errors': + return validateObject>(value, {}); + case 'pendingAction': + return validateConstantEnum(value, CONST.RED_BRICK_ROAD_PENDING_ACTION); + case 'pendingFields': + return validateObject>(value, { + reportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + reportActionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + parentReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + errors: CONST.RED_BRICK_ROAD_PENDING_ACTION, + sequenceNumber: CONST.RED_BRICK_ROAD_PENDING_ACTION, + actionName: CONST.RED_BRICK_ROAD_PENDING_ACTION, + actorAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + actor: CONST.RED_BRICK_ROAD_PENDING_ACTION, + person: CONST.RED_BRICK_ROAD_PENDING_ACTION, + created: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isLoading: CONST.RED_BRICK_ROAD_PENDING_ACTION, + avatar: CONST.RED_BRICK_ROAD_PENDING_ACTION, + automatic: CONST.RED_BRICK_ROAD_PENDING_ACTION, + shouldShow: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childReportName: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childType: CONST.RED_BRICK_ROAD_PENDING_ACTION, + accountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childOldestFourAccountIDs: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childCommenterCount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childLastVisibleActionCreated: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childVisibleActionCount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childManagerAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childOwnerAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childStatusNum: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childStateNum: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childLastMoneyRequestComment: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childLastActorAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childMoneyRequestCount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isFirstItem: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isAttachmentOnly: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isAttachmentWithText: CONST.RED_BRICK_ROAD_PENDING_ACTION, + receipt: CONST.RED_BRICK_ROAD_PENDING_ACTION, + lastModified: CONST.RED_BRICK_ROAD_PENDING_ACTION, + delegateAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + error: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childRecentReceiptTransactionIDs: CONST.RED_BRICK_ROAD_PENDING_ACTION, + linkMetadata: CONST.RED_BRICK_ROAD_PENDING_ACTION, + childReportNotificationPreference: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isNewestReportAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isOptimisticAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + adminAccountID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + whisperedToAccountIDs: CONST.RED_BRICK_ROAD_PENDING_ACTION, + reportActionTimestamp: CONST.RED_BRICK_ROAD_PENDING_ACTION, + timestamp: CONST.RED_BRICK_ROAD_PENDING_ACTION, + }); + case 'actionName': + return validateConstantEnum(value, CONST.REPORT.ACTIONS.TYPE); + case 'person': + return validateArray>(value, { + type: 'string', + text: 'string', + style: 'string', + }); + case 'childStatusNum': + return validateConstantEnum(value, CONST.REPORT.STATUS_NUM); + case 'childStateNum': + return validateConstantEnum(value, CONST.REPORT.STATE_NUM); + case 'receipt': + return validateObject>(value, { + state: 'string', + type: 'string', + name: 'string', + receiptID: 'string', + source: 'string', + filename: 'string', + reservationList: 'string', + }); + case 'childRecentReceiptTransactionIDs': + return validateObject>(value, {}, 'string'); + case 'linkMetadata': + return validateArray>(value, { + url: 'string', + image: 'object', + description: 'string', + title: 'string', + publisher: 'string', + logo: 'object', + }); + case 'childReportNotificationPreference': + return validateConstantEnum(value, CONST.REPORT.NOTIFICATION_PREFERENCE); + case 'whisperedToAccountIDs': + return validateArray(value, 'number'); + case 'message': + return unionValidation( + () => + validateArray>(value, { + text: 'string', + html: 'string', + type: 'string', + isDeletedParentAction: 'boolean', + policyID: 'string', + reportID: 'string', + currency: 'string', + amount: 'number', + style: 'string', + target: 'string', + href: 'string', + iconUrl: 'string', + isEdited: 'boolean', + isReversedTransaction: 'boolean', + whisperedTo: 'array', + moderationDecision: 'object', + translationKey: 'string', + taskReportID: 'string', + cancellationReason: 'string', + expenseReportID: 'string', + resolution: { + ...CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION, + ...CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION, + }, + deleted: 'string', + }), + () => + validateObject>(value, { + html: 'string', + text: 'string', + amount: 'string', + currency: 'string', + type: 'string', + policyID: 'string', + reportID: 'string', + isDeletedParentAction: 'boolean', + target: 'string', + style: 'string', + href: 'string', + iconUrl: 'boolean', + isEdited: 'boolean', + isReversedTransaction: 'boolean', + whisperedTo: 'array', + moderationDecision: 'object', + translationKey: 'string', + taskReportID: 'string', + cancellationReason: 'string', + expenseReportID: 'string', + resolution: { + ...CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION, + ...CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION, + }, + deleted: 'string', + }), + ); + case 'originalMessage': + return validateObject>(value, {}); + case 'previousMessage': + return unionValidation( + () => + validateObject>(value, { + html: 'string', + text: 'string', + amount: 'string', + currency: 'string', + type: 'string', + policyID: 'string', + reportID: 'string', + style: 'string', + target: 'string', + href: 'string', + iconUrl: 'string', + isEdited: 'boolean', + isDeletedParentAction: 'boolean', + isReversedTransaction: 'boolean', + whisperedTo: 'array', + moderationDecision: 'string', + translationKey: 'string', + taskReportID: 'string', + cancellationReason: 'string', + expenseReportID: 'string', + resolution: 'string', + deleted: 'string', + }), + () => + validateArray>(value, { + reportID: 'string', + html: 'string', + text: 'string', + amount: 'string', + currency: 'string', + type: 'string', + policyID: 'string', + style: 'string', + target: 'string', + href: 'string', + iconUrl: 'string', + isEdited: 'string', + isDeletedParentAction: 'string', + isReversedTransaction: 'string', + whisperedTo: 'string', + moderationDecision: 'string', + translationKey: 'string', + taskReportID: 'string', + cancellationReason: 'string', + expenseReportID: 'string', + resolution: 'string', + deleted: 'string', + }), + ); } - if (key === 'person') { - return validateArray(value, {}); +} + +/** + * Validates if a property of Transaction is of the expected type + * + * @param key - property key + * @param value - value provided by the user + */ +function validateTransactionDraftProperty(key: keyof Transaction, value: string) { + if (TRANSACTION_REQUIRED_PROPERTIES.includes(key) && isEmptyValue(value)) { + throw SyntaxError('debug.missingValue'); } - if (key === 'errors') { - return validateObject(value, {}); + switch (key) { + case 'reportID': + case 'currency': + case 'tag': + case 'category': + case 'merchant': + case 'taxCode': + case 'filename': + case 'modifiedCurrency': + case 'modifiedMerchant': + case 'transactionID': + case 'parentTransactionID': + case 'originalCurrency': + case 'actionableWhisperReportActionID': + case 'linkedTrackedExpenseReportID': + case 'bank': + case 'cardName': + case 'cardNumber': + return validateString(value); + case 'created': + case 'modifiedCreated': + return validateDate(value); + case 'isLoading': + case 'billable': + case 'reimbursable': + case 'participantsAutoAssigned': + case 'isFromGlobalCreate': + case 'hasEReceipt': + case 'shouldShowOriginalAmount': + case 'managedCard': + return validateBoolean(value); + case 'amount': + case 'taxAmount': + case 'modifiedAmount': + case 'cardID': + case 'originalAmount': + return validateNumber(value); + case 'iouRequestType': + return validateConstantEnum(value, CONST.IOU.REQUEST_TYPE); + case 'participants': + return validateArray>(value, { + accountID: 'number', + login: 'string', + displayName: 'string', + isPolicyExpenseChat: 'boolean', + isInvoiceRoom: 'boolean', + isOwnPolicyExpenseChat: 'boolean', + chatType: CONST.REPORT.CHAT_TYPE, + reportID: 'string', + policyID: 'string', + selected: 'boolean', + searchText: 'string', + alternateText: 'string', + firstName: 'string', + keyForList: 'string', + lastName: 'string', + phoneNumber: 'string', + text: 'string', + isSelected: 'boolean', + isSelfDM: 'boolean', + isSender: 'boolean', + iouType: CONST.IOU.TYPE, + ownerAccountID: 'number', + icons: 'array', + item: 'string', + }); + case 'errors': + return validateObject>(value, {}); + case 'errorFields': + return validateObject>( + value, + { + route: 'object', + }, + 'string', + ); + case 'pendingAction': + return validateConstantEnum(value, CONST.RED_BRICK_ROAD_PENDING_ACTION); + case 'pendingFields': + return validateObject>( + value, + { + comment: CONST.RED_BRICK_ROAD_PENDING_ACTION, + hold: CONST.RED_BRICK_ROAD_PENDING_ACTION, + waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isLoading: CONST.RED_BRICK_ROAD_PENDING_ACTION, + type: CONST.RED_BRICK_ROAD_PENDING_ACTION, + customUnit: CONST.RED_BRICK_ROAD_PENDING_ACTION, + source: CONST.RED_BRICK_ROAD_PENDING_ACTION, + originalTransactionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + splits: CONST.RED_BRICK_ROAD_PENDING_ACTION, + dismissedViolations: CONST.RED_BRICK_ROAD_PENDING_ACTION, + customUnitID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + customUnitRateID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + quantity: CONST.RED_BRICK_ROAD_PENDING_ACTION, + name: CONST.RED_BRICK_ROAD_PENDING_ACTION, + defaultP2PRate: CONST.RED_BRICK_ROAD_PENDING_ACTION, + distanceUnit: CONST.RED_BRICK_ROAD_PENDING_ACTION, + attendees: CONST.RED_BRICK_ROAD_PENDING_ACTION, + amount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + taxCode: CONST.RED_BRICK_ROAD_PENDING_ACTION, + billable: CONST.RED_BRICK_ROAD_PENDING_ACTION, + category: CONST.RED_BRICK_ROAD_PENDING_ACTION, + created: CONST.RED_BRICK_ROAD_PENDING_ACTION, + currency: CONST.RED_BRICK_ROAD_PENDING_ACTION, + errors: CONST.RED_BRICK_ROAD_PENDING_ACTION, + filename: CONST.RED_BRICK_ROAD_PENDING_ACTION, + iouRequestType: CONST.RED_BRICK_ROAD_PENDING_ACTION, + merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION, + modifiedAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + modifiedAttendees: CONST.RED_BRICK_ROAD_PENDING_ACTION, + modifiedCreated: CONST.RED_BRICK_ROAD_PENDING_ACTION, + modifiedCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION, + modifiedMerchant: CONST.RED_BRICK_ROAD_PENDING_ACTION, + modifiedWaypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION, + participantsAutoAssigned: CONST.RED_BRICK_ROAD_PENDING_ACTION, + participants: CONST.RED_BRICK_ROAD_PENDING_ACTION, + receipt: CONST.RED_BRICK_ROAD_PENDING_ACTION, + reportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + routes: CONST.RED_BRICK_ROAD_PENDING_ACTION, + transactionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + tag: CONST.RED_BRICK_ROAD_PENDING_ACTION, + isFromGlobalCreate: CONST.RED_BRICK_ROAD_PENDING_ACTION, + taxRate: CONST.RED_BRICK_ROAD_PENDING_ACTION, + parentTransactionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + reimbursable: CONST.RED_BRICK_ROAD_PENDING_ACTION, + cardID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + status: CONST.RED_BRICK_ROAD_PENDING_ACTION, + hasEReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION, + mccGroup: CONST.RED_BRICK_ROAD_PENDING_ACTION, + modifiedMCCGroup: CONST.RED_BRICK_ROAD_PENDING_ACTION, + originalAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + originalCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION, + splitShares: CONST.RED_BRICK_ROAD_PENDING_ACTION, + splitPayerAccountIDs: CONST.RED_BRICK_ROAD_PENDING_ACTION, + shouldShowOriginalAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION, + actionableWhisperReportActionID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + linkedTrackedExpenseReportAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + linkedTrackedExpenseReportID: CONST.RED_BRICK_ROAD_PENDING_ACTION, + bank: CONST.RED_BRICK_ROAD_PENDING_ACTION, + cardName: CONST.RED_BRICK_ROAD_PENDING_ACTION, + cardNumber: CONST.RED_BRICK_ROAD_PENDING_ACTION, + managedCard: CONST.RED_BRICK_ROAD_PENDING_ACTION, + }, + 'string', + ); + case 'receipt': + return validateObject>(value, { + type: 'string', + source: 'string', + name: 'string', + filename: 'string', + state: CONST.IOU.RECEIPT_STATE, + receiptID: 'number', + reservationList: 'array', + }); + case 'taxRate': + return validateObject>(value, { + keyForList: 'string', + text: 'string', + data: 'object', + }); + case 'status': + return validateConstantEnum(value, CONST.TRANSACTION.STATUS); + case 'comment': + return validateObject>(value, { + comment: 'string', + hold: 'string', + waypoints: 'object', + isLoading: 'boolean', + type: CONST.TRANSACTION.TYPE, + customUnit: 'object', + source: 'string', + originalTransactionID: 'string', + splits: 'array', + dismissedViolations: 'object', + }); + case 'attendees': + return validateArray>(value, { + email: 'string', + displayName: 'string', + avatarUrl: 'string', + accountID: 'number', + text: 'string', + login: 'string', + searchText: 'string', + selected: 'boolean', + iouType: CONST.IOU.TYPE, + reportID: 'string', + }); + case 'modifiedAttendees': + return validateArray>(value, { + email: 'string', + displayName: 'string', + avatarUrl: 'string', + accountID: 'number', + text: 'string', + login: 'string', + searchText: 'string', + selected: 'boolean', + iouType: CONST.IOU.TYPE, + reportID: 'string', + }); + case 'modifiedWaypoints': + return validateObject>( + value, + { + name: 'string', + address: 'string', + lat: 'number', + lng: 'number', + keyForList: 'string', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + street: 'string', + city: 'string', + state: 'string', + zipCode: 'string', + country: 'string', + street2: 'string', + }, + 'string', + ); + case 'routes': + return validateObject>( + value, + { + distance: 'number', + geometry: 'object', + }, + 'string', + ); + case 'mccGroup': + return validateConstantEnum(value, CONST.MCC_GROUPS); + case 'modifiedMCCGroup': + return validateConstantEnum(value, CONST.MCC_GROUPS); + case 'splitShares': + return validateObject>( + value, + { + amount: 'number', + isModified: 'boolean', + }, + 'number', + ); + case 'splitPayerAccountIDs': + return validateArray(value, 'number'); + case 'linkedTrackedExpenseReportAction': + return validateObject(value, { + accountID: 'number', + message: 'string', + created: 'string', + error: 'string', + avatar: 'string', + receipt: 'object', + reportID: 'string', + automatic: 'boolean', + reportActionID: 'string', + parentReportID: 'string', + errors: 'object', + isLoading: 'boolean', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, + pendingFields: 'object', + sequenceNumber: 'number', + actionName: CONST.REPORT.ACTIONS.TYPE, + actorAccountID: 'number', + actor: 'string', + person: 'array', + shouldShow: 'boolean', + childReportID: 'string', + childReportName: 'string', + childType: 'string', + childOldestFourAccountIDs: 'string', + childCommenterCount: 'number', + childLastVisibleActionCreated: 'string', + childVisibleActionCount: 'number', + childManagerAccountID: 'number', + childOwnerAccountID: 'number', + childStatusNum: CONST.REPORT.STATUS_NUM, + childStateNum: CONST.REPORT.STATE_NUM, + childLastMoneyRequestComment: 'string', + childLastActorAccountID: 'number', + childMoneyRequestCount: 'number', + isFirstItem: 'boolean', + isAttachmentOnly: 'boolean', + isAttachmentWithText: 'boolean', + lastModified: 'string', + delegateAccountID: 'number', + childRecentReceiptTransactionIDs: 'object', + linkMetadata: 'array', + childReportNotificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE, + isNewestReportAction: 'boolean', + isOptimisticAction: 'boolean', + adminAccountID: 'number', + whisperedToAccountIDs: 'array', + reportActionTimestamp: 'string', + timestamp: 'string', + originalMessage: 'object', + previousMessage: 'object', + }); } - if (key === 'originalMessage') { - return validateObject(value, {}); +} + +function validateTransactionViolationDraftProperty(key: keyof TransactionViolation, value: string) { + if (TRANSACTION_VIOLATION_REQUIRED_PROPERTIES.includes(key) && isEmptyValue(value)) { + throw SyntaxError('debug.missingValue'); } - if (key === 'childRecentReceiptTransactionIDs') { - return validateObject(value, {}, 'string'); + switch (key) { + case 'type': + return validateConstantEnum(value, CONST.VIOLATION_TYPES); + + case 'name': + return validateConstantEnum(value, CONST.VIOLATIONS); + + case 'data': + return validateObject>(value, { + rejectedBy: 'string', + rejectReason: 'string', + formattedLimit: 'string', + surcharge: 'number', + invoiceMarkup: 'number', + maxAge: 'number', + tagName: 'string', + category: 'string', + brokenBankConnection: 'boolean', + isAdmin: 'boolean', + email: 'string', + isTransactionOlderThan7Days: 'boolean', + member: 'string', + taxName: 'string', + tagListIndex: 'number', + tagListName: 'string', + errorIndexes: 'array', + pendingPattern: 'string', + type: CONST.MODIFIED_AMOUNT_VIOLATION_DATA, + displayPercentVariance: 'number', + duplicates: 'array', + rterType: CONST.RTER_VIOLATION_TYPES, + tooltip: 'string', + }); + case 'showInReview': + return validateBoolean(value); } - validateString(value); } /** @@ -562,7 +1263,7 @@ function validateReportActionJSON(json: string) { }); Object.entries(parsedReportAction).forEach(([key, val]) => { try { - if (val !== 'undefined' && REPORT_ACTION_NUMBER_PROPERTIES.includes(key as keyof ReportAction) && typeof val !== 'number') { + if (!isEmptyValue(val as string) && REPORT_ACTION_NUMBER_PROPERTIES.includes(key as keyof ReportAction) && typeof val !== 'number') { throw new NumberError(); } validateReportActionDraftProperty(key as keyof ReportAction, onyxDataToString(val)); @@ -573,6 +1274,25 @@ function validateReportActionJSON(json: string) { }); } +function validateTransactionViolationJSON(json: string) { + const parsedTransactionViolation = parseJSON(json) as TransactionViolation; + TRANSACTION_VIOLATION_REQUIRED_PROPERTIES.forEach((key) => { + if (parsedTransactionViolation[key] !== undefined) { + return; + } + + throw new SyntaxError('debug.missingProperty', {cause: {propertyName: key}}); + }); + Object.entries(parsedTransactionViolation).forEach(([key, val]) => { + try { + validateTransactionViolationDraftProperty(key as keyof TransactionViolation, onyxDataToString(val)); + } catch (e) { + const {cause} = e as SyntaxError & {cause: {expectedValues: string}}; + throw new SyntaxError('debug.invalidProperty', {cause: {propertyName: key, expectedType: cause.expectedValues}}); + } + }); +} + /** * Gets the reason for showing LHN row */ @@ -648,6 +1368,16 @@ function getReasonAndReportActionForRBRInLHNRow(report: Report, reportActions: O return null; } +function getTransactionID(report: OnyxEntry, reportActions: OnyxEntry) { + const transactionID = TransactionUtils.getTransactionID(report?.reportID ?? '-1'); + + return Number(transactionID) > 0 + ? transactionID + : Object.values(reportActions ?? {}) + .map((reportAction) => ReportActionsUtils.getLinkedTransactionID(reportAction)) + .find(Boolean); +} + const DebugUtils = { stringifyJSON, onyxDataToDraftData, @@ -664,12 +1394,17 @@ const DebugUtils = { validateString, validateReportDraftProperty, validateReportActionDraftProperty, + validateTransactionDraftProperty, + validateTransactionViolationDraftProperty, validateReportActionJSON, + validateTransactionViolationJSON, getReasonForShowingRowInLHN, getReasonAndReportActionForGBRInLHNRow, getReasonAndReportActionForRBRInLHNRow, + getTransactionID, REPORT_ACTION_REQUIRED_PROPERTIES, REPORT_REQUIRED_PROPERTIES, + TRANSACTION_REQUIRED_PROPERTIES, }; export type {ObjectType, OnyxDataType}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 8c0d45e8c313..2b6d0b84b460 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -666,6 +666,9 @@ const DebugModalStackNavigator = createModalStackNavigator({ [SCREENS.DEBUG.REPORT_ACTION_CREATE]: () => require('../../../../pages/Debug/ReportAction/DebugReportActionCreatePage').default, [SCREENS.DEBUG.DETAILS_CONSTANT_PICKER_PAGE]: () => require('../../../../pages/Debug/DebugDetailsConstantPickerPage').default, [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: () => require('../../../../pages/Debug/DebugDetailsDateTimePickerPage').default, + [SCREENS.DEBUG.TRANSACTION]: () => require('../../../../pages/Debug/Transaction/DebugTransactionPage').default, + [SCREENS.DEBUG.TRANSACTION_VIOLATION_CREATE]: () => require('../../../../pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage').default, + [SCREENS.DEBUG.TRANSACTION_VIOLATION]: () => require('../../../../pages/Debug/TransactionViolation/DebugTransactionViolationPage').default, }); export { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index e0cd018086bd..8473b8fa49c2 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1410,6 +1410,42 @@ const config: LinkingOptions['config'] = { path: ROUTES.DETAILS_DATE_TIME_PICKER_PAGE.route, exact: true, }, + [SCREENS.DEBUG.TRANSACTION]: { + path: ROUTES.DEBUG_TRANSACTION.route, + exact: true, + screens: { + details: { + path: ROUTES.DEBUG_TRANSACTION_TAB_DETAILS.route, + exact: true, + }, + json: { + path: ROUTES.DEBUG_TRANSACTION_TAB_JSON.route, + exact: true, + }, + violations: { + path: ROUTES.DEBUG_TRANSACTION_TAB_VIOLATIONS.route, + exact: true, + }, + }, + }, + [SCREENS.DEBUG.TRANSACTION_VIOLATION_CREATE]: { + path: ROUTES.DEBUG_TRANSACTION_VIOLATION_CREATE.route, + exact: true, + }, + [SCREENS.DEBUG.TRANSACTION_VIOLATION]: { + path: ROUTES.DEBUG_TRANSACTION_VIOLATION.route, + exact: true, + screens: { + details: { + path: ROUTES.DEBUG_TRANSACTION_VIOLATION_TAB_DETAILS.route, + exact: true, + }, + json: { + path: ROUTES.DEBUG_TRANSACTION_VIOLATION_TAB_JSON.route, + exact: true, + }, + }, + }, }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 798e77d86ecc..9abd3f78a3f9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1626,8 +1626,10 @@ type DebugParamList = { reportID: string; }; [SCREENS.DEBUG.DETAILS_CONSTANT_PICKER_PAGE]: { + formType: string; fieldName: string; fieldValue?: string; + policyID?: string; backTo?: string; }; [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: { @@ -1635,6 +1637,16 @@ type DebugParamList = { fieldValue?: string; backTo?: string; }; + [SCREENS.DEBUG.TRANSACTION]: { + transactionID: string; + }; + [SCREENS.DEBUG.TRANSACTION_VIOLATION_CREATE]: { + transactionID: string; + }; + [SCREENS.DEBUG.TRANSACTION_VIOLATION]: { + transactionID: string; + index: string; + }; }; type RootStackParamList = PublicScreensParamList & AuthScreensParamList & LeftModalNavigatorParamList; diff --git a/src/libs/actions/Debug.ts b/src/libs/actions/Debug.ts index 5047ab063b7e..4c3479ee9741 100644 --- a/src/libs/actions/Debug.ts +++ b/src/libs/actions/Debug.ts @@ -7,11 +7,11 @@ function resetDebugDetailsDraftForm() { Onyx.set(ONYXKEYS.FORMS.DEBUG_DETAILS_FORM_DRAFT, null); } -function mergeDebugData(onyxKey: TKey, onyxValue: OnyxMergeInput) { - Onyx.merge(onyxKey, onyxValue); +function setDebugData(onyxKey: TKey, onyxValue: OnyxMergeInput) { + Onyx.set(onyxKey, onyxValue); } export default { resetDebugDetailsDraftForm, - mergeDebugData, + setDebugData, }; diff --git a/src/pages/Debug/ConstantPicker.tsx b/src/pages/Debug/ConstantPicker.tsx new file mode 100644 index 000000000000..564b2ea3d710 --- /dev/null +++ b/src/pages/Debug/ConstantPicker.tsx @@ -0,0 +1,69 @@ +import isObject from 'lodash/isObject'; +import React, {useMemo, useState} from 'react'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import useLocalize from '@hooks/useLocalize'; +import type {DebugForms} from './const'; +import {DETAILS_CONSTANT_FIELDS} from './const'; + +type ConstantPickerProps = { + formType: string; + /** The form to object the constant list of options */ + + /** Constant name to get list of options */ + fieldName: string; + + /** Current selected constant */ + fieldValue?: string; + + /** Callback to submit the selected constant */ + onSubmit: (item: ListItem) => void; +}; + +function ConstantPicker({formType, fieldName, fieldValue, onSubmit}: ConstantPickerProps) { + const {translate} = useLocalize(); + const [searchValue, setSearchValue] = useState(''); + const sections: ListItem[] = useMemo( + () => + Object.entries(DETAILS_CONSTANT_FIELDS[formType as DebugForms].find((field) => field.fieldName === fieldName)?.options ?? {}) + .reduce((acc: Array<[string, string]>, [key, value]) => { + // Option has multiple constants, so we need to flatten these into separate options + if (isObject(value)) { + acc.push(...Object.entries(value)); + return acc; + } + acc.push([key, String(value)]); + return acc; + }, []) + .map( + ([key, value]) => + ({ + text: value, + keyForList: key, + isSelected: value === fieldValue, + searchText: value, + } satisfies ListItem), + ) + .filter(({searchText}) => searchText.toLowerCase().includes(searchValue.toLowerCase())), + [fieldName, fieldValue, formType, searchValue], + ); + const selectedOptionKey = useMemo(() => sections.filter((option) => option.searchText === fieldValue).at(0)?.keyForList, [sections, fieldValue]); + + return ( + + ); +} + +ConstantPicker.default = 'ConstantPicker'; + +export default ConstantPicker; diff --git a/src/pages/Debug/ConstantSelector.tsx b/src/pages/Debug/ConstantSelector.tsx index d6a3c0cfb4b1..c2df1f3e3e2a 100644 --- a/src/pages/Debug/ConstantSelector.tsx +++ b/src/pages/Debug/ConstantSelector.tsx @@ -1,5 +1,6 @@ import {useRoute} from '@react-navigation/native'; import React, {useEffect} from 'react'; +import type {ValueOf} from 'type-fest'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -21,9 +22,14 @@ type ConstantSelectorProps = { /** inputID used by the Form component */ // eslint-disable-next-line react/no-unused-prop-types inputID: string; + + /** Type of debug form - required to access constant field options for a specific form */ + formType: ValueOf; + + policyID?: string; }; -function ConstantSelector({errorText = '', name, value, onInputChange}: ConstantSelectorProps) { +function ConstantSelector({formType, policyID, errorText = '', name, value, onInputChange}: ConstantSelectorProps) { const fieldValue = (useRoute().params as Record | undefined)?.[name]; useEffect(() => { @@ -49,7 +55,7 @@ function ConstantSelector({errorText = '', name, value, onInputChange}: Constant brickRoadIndicator={errorText ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={errorText} onPress={() => { - Navigation.navigate(ROUTES.DETAILS_CONSTANT_PICKER_PAGE.getRoute(name, value, Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.DETAILS_CONSTANT_PICKER_PAGE.getRoute(formType, name, value, policyID, Navigation.getActiveRoute())); }} shouldShowRightIcon /> diff --git a/src/pages/Debug/DebugDetails.tsx b/src/pages/Debug/DebugDetails.tsx index 6ee14660dbe9..60126ef1937a 100644 --- a/src/pages/Debug/DebugDetails.tsx +++ b/src/pages/Debug/DebugDetails.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; import FormProvider from '@components/Form/FormProvider'; @@ -14,18 +15,24 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import type {ObjectType, OnyxDataType} from '@libs/DebugUtils'; import DebugUtils from '@libs/DebugUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as TagsOptionsListUtils from '@libs/TagsOptionsListUtils'; import Debug from '@userActions/Debug'; +import type CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, ReportAction} from '@src/types/onyx'; -import type {DetailsConstantFieldsKeys, DetailsDatetimeFieldsKeys, DetailsDisabledKeys} from './const'; +import TRANSACTION_FORM_INPUT_IDS from '@src/types/form/DebugTransactionForm'; +import type {Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx'; import {DETAILS_CONSTANT_FIELDS, DETAILS_DATETIME_FIELDS, DETAILS_DISABLED_KEYS} from './const'; import ConstantSelector from './ConstantSelector'; import DateTimeSelector from './DateTimeSelector'; type DebugDetailsProps = { + /** Type of debug form - required to access constant field options for a specific form */ + formType: ValueOf; + /** The report or report action data to be displayed and editted. */ - data: OnyxEntry | OnyxEntry; + data: OnyxEntry | OnyxEntry | OnyxEntry | OnyxEntry; children?: React.ReactNode; @@ -40,10 +47,13 @@ type DebugDetailsProps = { validate: (key: any, value: string) => void; }; -function DebugDetails({data, children, onSave, onDelete, validate}: DebugDetailsProps) { +function DebugDetails({formType, data, children, onSave, onDelete, validate}: DebugDetailsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [formDraftData] = useOnyx(ONYXKEYS.FORMS.DEBUG_DETAILS_FORM_DRAFT); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${(data as OnyxEntry)?.reportID ?? ''}`); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`); + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); const booleanFields = useMemo( () => Object.entries(data ?? {}) @@ -54,9 +64,15 @@ function DebugDetails({data, children, onSave, onDelete, validate}: DebugDetails const constantFields = useMemo( () => Object.entries(data ?? {}) - .filter((entry): entry is [string, string] => DETAILS_CONSTANT_FIELDS.includes(entry[0] as DetailsConstantFieldsKeys)) + .filter((entry): entry is [string, string] => { + // Tag picker needs to be hidden when the policy has no tags available to pick + if (entry[0] === TRANSACTION_FORM_INPUT_IDS.TAG && !TagsOptionsListUtils.hasEnabledTags(policyTagLists)) { + return false; + } + return DETAILS_CONSTANT_FIELDS[formType].some(({fieldName}) => fieldName === entry[0]); + }) .sort((a, b) => a[0].localeCompare(b[0])), - [data], + [data, formType, policyTagLists], ); const numberFields = useMemo( () => @@ -69,19 +85,16 @@ function DebugDetails({data, children, onSave, onDelete, validate}: DebugDetails () => Object.entries(data ?? {}) .filter( - (entry): entry is [string, string | ObjectType] => + (entry): entry is [string, string | ObjectType>] => (typeof entry[1] === 'string' || typeof entry[1] === 'object') && - !DETAILS_CONSTANT_FIELDS.includes(entry[0] as DetailsConstantFieldsKeys) && - !DETAILS_DATETIME_FIELDS.includes(entry[0] as DetailsDatetimeFieldsKeys), + !DETAILS_CONSTANT_FIELDS[formType].some(({fieldName}) => fieldName === entry[0]) && + !DETAILS_DATETIME_FIELDS.includes(entry[0]), ) .map(([key, value]) => [key, DebugUtils.onyxDataToString(value)]) .sort((a, b) => (a.at(0) ?? '').localeCompare(b.at(0) ?? '')), - [data], - ); - const dateTimeFields = useMemo( - () => Object.entries(data ?? {}).filter((entry): entry is [string, string] => DETAILS_DATETIME_FIELDS.includes(entry[0] as DetailsDatetimeFieldsKeys)), - [data], + [data, formType], ); + const dateTimeFields = useMemo(() => Object.entries(data ?? {}).filter((entry): entry is [string, string] => DETAILS_DATETIME_FIELDS.includes(entry[0])), [data]); const validator = useCallback( (values: FormOnyxValues): FormInputErrors => { @@ -161,7 +174,7 @@ function DebugDetails({data, children, onSave, onDelete, validate}: DebugDetails numberOfLines={numberOfLines} multiline={numberOfLines > 1} defaultValue={value} - disabled={DETAILS_DISABLED_KEYS.includes(key as DetailsDisabledKeys)} + disabled={DETAILS_DISABLED_KEYS.includes(key)} shouldInterceptSwipe /> ); @@ -179,11 +192,11 @@ function DebugDetails({data, children, onSave, onDelete, validate}: DebugDetails forceActiveLabel label={key} defaultValue={String(value)} - disabled={DETAILS_DISABLED_KEYS.includes(key as DetailsDisabledKeys)} + disabled={DETAILS_DISABLED_KEYS.includes(key)} shouldInterceptSwipe /> ))} - {numberFields.length === 0 && {translate('debug.none')}} + {numberFields.length === 0 && {translate('debug.none')}} {translate('debug.constantFields')} @@ -192,9 +205,11 @@ function DebugDetails({data, children, onSave, onDelete, validate}: DebugDetails key={key} InputComponent={ConstantSelector} inputID={key} + formType={formType} name={key} shouldSaveDraft defaultValue={String(value)} + policyID={report?.policyID} /> ))} {constantFields.length === 0 && {translate('debug.none')}} @@ -225,7 +240,7 @@ function DebugDetails({data, children, onSave, onDelete, validate}: DebugDetails defaultValue={value} /> ))} - {booleanFields.length === 0 && {translate('debug.none')}} + {booleanFields.length === 0 && {translate('debug.none')}} {translate('debug.hint')} diff --git a/src/pages/Debug/DebugDetailsConstantPickerPage.tsx b/src/pages/Debug/DebugDetailsConstantPickerPage.tsx index a98ef9963542..fca11799fd5d 100644 --- a/src/pages/Debug/DebugDetailsConstantPickerPage.tsx +++ b/src/pages/Debug/DebugDetailsConstantPickerPage.tsx @@ -1,86 +1,98 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import isObject from 'lodash/isObject'; -import React, {useMemo, useState} from 'react'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; +import CategoryPicker from '@components/CategoryPicker'; +import CurrencySelectionList from '@components/CurrencySelectionList'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {DebugParamList} from '@libs/Navigation/types'; import {appendParam} from '@libs/Url'; +import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; -import {DETAILS_CONSTANT_OPTIONS} from './const'; +import TRANSACTION_FORM_INPUT_IDS from '@src/types/form/DebugTransactionForm'; +import ConstantPicker from './ConstantPicker'; +import DebugTagPicker from './DebugTagPicker'; type DebugDetailsConstantPickerPageProps = StackScreenProps; function DebugDetailsConstantPickerPage({ route: { - params: {fieldName, fieldValue, backTo = ''}, + params: {formType, fieldName, fieldValue, policyID = '', backTo = ''}, }, navigation, }: DebugDetailsConstantPickerPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [searchValue, setSearchValue] = useState(''); - const sections: ListItem[] = useMemo( - () => - Object.entries(DETAILS_CONSTANT_OPTIONS[fieldName as keyof typeof DETAILS_CONSTANT_OPTIONS]) - .reduce((acc: Array<[string, string]>, [key, value]) => { - // Option has multiple constants, so we need to flatten these into separate options - if (isObject(value)) { - acc.push(...Object.entries(value)); - return acc; - } - acc.push([key, value as string]); - return acc; - }, []) - .map( - ([key, value]) => - ({ - text: value, - keyForList: key, - isSelected: value === fieldValue, - searchText: value, - } satisfies ListItem), - ) - .filter(({searchText}) => searchText.toLowerCase().includes(searchValue.toLowerCase())), - [fieldName, fieldValue, searchValue], + const onSubmit = useCallback( + (item: ListItem) => { + const value = item.text === fieldValue ? '' : item.text ?? ''; + // Check the navigation state and "backTo" parameter to decide navigation behavior + if (navigation.getState().routes.length === 1 && !backTo) { + // If there is only one route and "backTo" is empty, go back in navigation + Navigation.goBack(); + } else if (!!backTo && navigation.getState().routes.length === 1) { + // If "backTo" is not empty and there is only one route, go back to the specific route defined in "backTo" with a country parameter + Navigation.goBack(appendParam(backTo, fieldName, value)); + } else { + // Otherwise, navigate to the specific route defined in "backTo" with a country parameter + Navigation.navigate(appendParam(backTo, fieldName, value)); + } + }, + [backTo, fieldName, fieldValue, navigation], ); - const onSubmit = (item: ListItem) => { - const value = item.text === fieldValue ? '' : item.text ?? ''; - // Check the navigation state and "backTo" parameter to decide navigation behavior - if (navigation.getState().routes.length === 1 && !backTo) { - // If there is only one route and "backTo" is empty, go back in navigation - Navigation.goBack(); - } else if (!!backTo && navigation.getState().routes.length === 1) { - // If "backTo" is not empty and there is only one route, go back to the specific route defined in "backTo" with a country parameter - Navigation.goBack(appendParam(backTo, fieldName, value)); - } else { - // Otherwise, navigate to the specific route defined in "backTo" with a country parameter - Navigation.navigate(appendParam(backTo, fieldName, value)); + + const renderPicker = useCallback(() => { + if (([TRANSACTION_FORM_INPUT_IDS.CURRENCY, TRANSACTION_FORM_INPUT_IDS.MODIFIED_CURRENCY, TRANSACTION_FORM_INPUT_IDS.ORIGINAL_CURRENCY] as string[]).includes(fieldName)) { + return ( + + onSubmit({ + text: currencyCode, + }) + } + searchInputLabel={translate('common.search')} + /> + ); } - }; - const selectedOptionKey = useMemo(() => sections.filter((option) => option.searchText === fieldValue).at(0)?.keyForList, [sections, fieldValue]); + if (formType === CONST.DEBUG.FORMS.TRANSACTION) { + if (fieldName === TRANSACTION_FORM_INPUT_IDS.CATEGORY) { + return ( + + ); + } + if (fieldName === TRANSACTION_FORM_INPUT_IDS.TAG) { + return ( + + ); + } + } + + return ( + + ); + }, [fieldName, fieldValue, formType, onSubmit, policyID, translate]); return ( - - - + {renderPicker()} ); } diff --git a/src/pages/Debug/DebugTagPicker.tsx b/src/pages/Debug/DebugTagPicker.tsx new file mode 100644 index 000000000000..1aa24d359a3a --- /dev/null +++ b/src/pages/Debug/DebugTagPicker.tsx @@ -0,0 +1,82 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import type {ListItem} from '@components/SelectionList/types'; +import TagPicker from '@components/TagPicker'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as IOUUtils from '@libs/IOUUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type DebugTagPickerProps = { + /** The policyID we are getting tags for */ + policyID: string; + + /** Current tag name */ + tagName?: string; + + /** Callback to submit the selected tag */ + onSubmit: (item: ListItem) => void; +}; + +function DebugTagPicker({policyID, tagName = '', onSubmit}: DebugTagPickerProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [newTagName, setNewTagName] = useState(tagName); + const selectedTags = useMemo(() => TransactionUtils.getTagArrayFromName(newTagName), [newTagName]); + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + + const updateTagName = useCallback( + (index: number) => + ({text}: ListItem) => { + const newTag = text === selectedTags.at(index) ? undefined : text; + const updatedTagName = IOUUtils.insertTagIntoTransactionTagsString(newTagName, newTag ?? '', index); + if (policyTagLists.length === 1) { + return onSubmit({text: updatedTagName}); + } + setNewTagName(updatedTagName); + }, + [newTagName, onSubmit, policyTagLists.length, selectedTags], + ); + + const submitTag = useCallback(() => { + onSubmit({text: newTagName}); + }, [newTagName, onSubmit]); + + return ( + + + {policyTagLists.map(({name}, index) => ( + + {policyTagLists.length > 1 && {name}} + + + ))} + + {policyTagLists.length > 1 && ( + +