diff --git a/src/CONST.ts b/src/CONST.ts index b977c9dbd6d4..5c8cd1b8f038 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2638,6 +2638,7 @@ const CONST = { INDENTS: ' ', PARENT_CHILD_SEPARATOR: ': ', CATEGORY_LIST_THRESHOLD: 8, + TAG_LIST_THRESHOLD: 8, DEMO_PAGES: { SAASTR: 'SaaStrDemoSetup', SBE: 'SbeDemoSetup', diff --git a/src/components/CategoryPicker/index.js b/src/components/CategoryPicker/index.js index 91c7e82e7887..9d4e0747cc18 100644 --- a/src/components/CategoryPicker/index.js +++ b/src/components/CategoryPicker/index.js @@ -46,7 +46,7 @@ function CategoryPicker({policyCategories, reportID, iouType, iou, policyRecentl }, [policyCategories, selectedOptions, isCategoriesCountBelowThreshold]); const sections = useMemo( - () => OptionsListUtils.getNewChatOptions({}, {}, [], searchValue, selectedOptions, [], false, false, true, policyCategories, policyRecentlyUsedCategories, false).categoryOptions, + () => OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, true, policyCategories, policyRecentlyUsedCategories, false).categoryOptions, [policyCategories, policyRecentlyUsedCategories, searchValue, selectedOptions], ); diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index b00d4b1a62a5..4703ca099c7c 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -201,6 +201,7 @@ function MoneyRequestConfirmationList(props) { const tagList = lodashGet(props.policyTags, [tagListKey, 'tags'], []); const tagListName = lodashGet(props.policyTags, [tagListKey, 'name'], ''); const canUseTags = Permissions.canUseTags(props.betas); + const shouldShowTags = canUseTags && _.any(tagList, (tag) => tag.enabled); const hasRoute = TransactionUtils.hasRoute(transaction); const isDistanceRequestWithoutRoute = props.isDistanceRequest && !hasRoute; @@ -527,7 +528,7 @@ function MoneyRequestConfirmationList(props) { disabled={didConfirm || props.isReadOnly} /> )} - {canUseTags && !!tagList && ( + {shouldShowTags && ( policyTag.enabled)); + const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD; + + const shouldShowTextInput = !isTagsCountBelowThreshold; const selectedOptions = useMemo(() => { - if (!iou.tag) { + if (!selectedTag) { return []; } return [ { - name: iou.tag, + name: selectedTag, enabled: true, + accountID: null, }, ]; - }, [iou.tag]); - - // Only shows one section, which will be the default behavior if there are - // less than 8 policy tags - // TODO: support sections with search - const sections = useMemo(() => { - const tagList = _.chain(lodashGet(policyTags, [tag, 'tags'], {})) - .values() - .map((t) => ({ - text: t.name, - keyForList: t.name, - tooltipText: t.name, - })) - .value(); + }, [selectedTag]); - return [ - { - data: tagList, - }, - ]; - }, [policyTags, tag]); + const initialFocusedIndex = useMemo(() => { + if (isTagsCountBelowThreshold && selectedOptions.length > 0) { + return _.chain(policyTagList) + .values() + .findIndex((policyTag) => policyTag.name === selectedOptions[0].name, true) + .value(); + } - const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, ''); + return 0; + }, [policyTagList, selectedOptions, isTagsCountBelowThreshold]); - const navigateBack = () => { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); - }; + const sections = useMemo( + () => + OptionsListUtils.getFilteredOptions({}, {}, [], searchValue, selectedOptions, [], false, false, false, {}, [], true, policyTagList, policyRecentlyUsedTagsList, false).tagOptions, + [searchValue, selectedOptions, policyTagList, policyRecentlyUsedTagsList], + ); - const updateTag = () => { - // TODO: add logic to save the selected tag - navigateBack(); - }; + const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, ''); return ( ); } @@ -84,7 +84,4 @@ export default withOnyx({ policyRecentlyUsedTags: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, }, - iou: { - key: ONYXKEYS.IOU, - }, })(TagPicker); diff --git a/src/components/TagPicker/tagPickerPropTypes.js b/src/components/TagPicker/tagPickerPropTypes.js index ad57a0409f15..a5d94605a76a 100644 --- a/src/components/TagPicker/tagPickerPropTypes.js +++ b/src/components/TagPicker/tagPickerPropTypes.js @@ -1,22 +1,18 @@ import PropTypes from 'prop-types'; import tagPropTypes from '../tagPropTypes'; -import {iouPropTypes, iouDefaultProps} from '../../pages/iou/propTypes'; const propTypes = { - /** The report ID of the IOU */ - reportID: PropTypes.string.isRequired, - /** The policyID we are getting tags for */ policyID: PropTypes.string.isRequired, + /** The selected tag of the money request */ + selectedTag: PropTypes.string.isRequired, + /** The name of tag list we are getting tags for */ tag: PropTypes.string.isRequired, - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string.isRequired, - /** Callback to submit the selected tag */ - onSubmit: PropTypes.func, + onSubmit: PropTypes.func.isRequired, /* Onyx Props */ /** Collection of tags attached to a policy */ @@ -29,15 +25,11 @@ const propTypes = { /** List of recently used tags */ policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, }; const defaultProps = { policyTags: {}, policyRecentlyUsedTags: {}, - iou: iouDefaultProps, }; export {propTypes, defaultProps}; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 8587ae1933db..45bc6dffd67a 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -594,7 +594,7 @@ function isCurrentUser(userDetails) { /** * Build the options for the category tree hierarchy via indents * - * @param {Object[]} options - an initial strings array + * @param {Object[]} options - an initial object array * @param {Boolean} options[].enabled - a flag to enable/disable option in a list * @param {String} options[].name - a name of an option * @param {Boolean} [isOneLine] - a flag to determine if text should be one line @@ -731,6 +731,124 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt return categorySections; } +/** + * Transforms the provided tags into objects with a specific structure. + * + * @param {Object[]} tags - an initial tag array + * @param {Boolean} tags[].enabled - a flag to enable/disable option in a list + * @param {String} tags[].name - a name of an option + * @returns {Array} + */ +function getTagsOptions(tags) { + return _.map(tags, (tag) => ({ + text: tag.name, + keyForList: tag.name, + searchText: tag.name, + tooltipText: tag.name, + isDisabled: !tag.enabled, + })); +} + +/** + * Build the section list for tags + * + * @param {Object[]} tags + * @param {String} tags[].name + * @param {Boolean} tags[].enabled + * @param {String[]} recentlyUsedTags + * @param {Object[]} selectedOptions + * @param {String} selectedOptions[].name + * @param {String} searchInputValue + * @param {Number} maxRecentReportsToShow + * @returns {Array} + */ +function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow) { + const tagSections = []; + const enabledTags = _.filter(tags, (tag) => tag.enabled); + const numberOfTags = _.size(enabledTags); + let indexOffset = 0; + + if (!_.isEmpty(searchInputValue)) { + const searchTags = _.filter(enabledTags, (tag) => tag.name.toLowerCase().includes(searchInputValue.toLowerCase())); + + tagSections.push({ + // "Search" section + title: '', + shouldShow: false, + indexOffset, + data: getTagsOptions(searchTags), + }); + + return tagSections; + } + + if (numberOfTags < CONST.TAG_LIST_THRESHOLD) { + tagSections.push({ + // "All" section when items amount less than the threshold + title: '', + shouldShow: false, + indexOffset, + data: getTagsOptions(enabledTags), + }); + + return tagSections; + } + + const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); + const filteredRecentlyUsedTags = _.map( + _.filter(recentlyUsedTags, (recentlyUsedTag) => { + const tagObject = _.find(tags, (tag) => tag.name === recentlyUsedTag); + return Boolean(tagObject && tagObject.enabled) && !_.includes(selectedOptionNames, recentlyUsedTag); + }), + (tag) => ({name: tag, enabled: true}), + ); + const filteredTags = _.filter(enabledTags, (tag) => !_.includes(selectedOptionNames, tag.name)); + + if (!_.isEmpty(selectedOptions)) { + const selectedTagOptions = _.map(selectedOptions, (option) => { + const tagObject = _.find(tags, (tag) => tag.name === option.name); + return { + name: option.name, + enabled: Boolean(tagObject && tagObject.enabled), + }; + }); + + tagSections.push({ + // "Selected" section + title: '', + shouldShow: false, + indexOffset, + data: getTagsOptions(selectedTagOptions), + }); + + indexOffset += selectedOptions.length; + } + + if (!_.isEmpty(filteredRecentlyUsedTags)) { + const cutRecentlyUsedTags = filteredRecentlyUsedTags.slice(0, maxRecentReportsToShow); + + tagSections.push({ + // "Recent" section + title: Localize.translateLocal('common.recent'), + shouldShow: true, + indexOffset, + data: getTagsOptions(cutRecentlyUsedTags), + }); + + indexOffset += filteredRecentlyUsedTags.length; + } + + tagSections.push({ + // "All" section when items amount more than the threshold + title: Localize.translateLocal('common.all'), + shouldShow: true, + indexOffset, + data: getTagsOptions(filteredTags), + }); + + return tagSections; +} + /** * Build the options * @@ -767,6 +885,9 @@ function getOptions( includeCategories = false, categories = {}, recentlyUsedCategories = [], + includeTags = false, + tags = {}, + recentlyUsedTags = [], canInviteUser = true, }, ) { @@ -779,6 +900,20 @@ function getOptions( userToInvite: null, currentUserOption: null, categoryOptions, + tagOptions: [], + }; + } + + if (includeTags) { + const tagOptions = getTagListSections(_.values(tags), recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow); + + return { + recentReports: [], + personalDetails: [], + userToInvite: null, + currentUserOption: null, + categoryOptions: [], + tagOptions, }; } @@ -789,6 +924,7 @@ function getOptions( userToInvite: null, currentUserOption: null, categoryOptions: [], + tagOptions: [], }; } @@ -1043,6 +1179,7 @@ function getOptions( userToInvite: canInviteUser ? userToInvite : null, currentUserOption, categoryOptions: [], + tagOptions: [], }; } @@ -1127,10 +1264,13 @@ function getIOUConfirmationOptionsFromParticipants(participants, amountText) { * @param {boolean} [includeCategories] * @param {Object} [categories] * @param {Array} [recentlyUsedCategories] + * @param {boolean} [includeTags] + * @param {Object} [tags] + * @param {Array} [recentlyUsedTags] * @param {boolean} [canInviteUser] * @returns {Object} */ -function getNewChatOptions( +function getFilteredOptions( reports, personalDetails, betas = [], @@ -1142,6 +1282,9 @@ function getNewChatOptions( includeCategories = false, categories = {}, recentlyUsedCategories = [], + includeTags = false, + tags = {}, + recentlyUsedTags = [], canInviteUser = true, ) { return getOptions(reports, personalDetails, { @@ -1157,6 +1300,9 @@ function getNewChatOptions( includeCategories, categories, recentlyUsedCategories, + includeTags, + tags, + recentlyUsedTags, canInviteUser, }); } @@ -1312,7 +1458,7 @@ export { isCurrentUser, isPersonalDetailsReady, getSearchOptions, - getNewChatOptions, + getFilteredOptions, getShareDestinationOptions, getMemberInviteOptions, getHeaderMessage, diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 70583175f115..cea530f6f47a 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -34,6 +34,7 @@ Onyx.connect({ * @param {String} [filename] * @param {String} [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated. * @param {String} [category] + * @param {String} [tag] * @param {Boolean} [billable] * @returns {Object} */ @@ -50,6 +51,7 @@ function buildOptimisticTransaction( filename = '', existingTransactionID = null, category = '', + tag = '', billable = false, ) { // transactionIDs are random, positive, 64-bit numeric strings. @@ -79,6 +81,7 @@ function buildOptimisticTransaction( receipt, filename, category, + tag, billable, }; } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 9e8dadbd53b7..6212b48e806f 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -51,6 +51,20 @@ Onyx.connect({ callback: (val) => (allRecentlyUsedCategories = val), }); +let allRecentlyUsedTags = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allRecentlyUsedTags = {}; + return; + } + + allRecentlyUsedTags = value; + }, +}); + let userAccountID = ''; let currentUserEmail = ''; Onyx.connect({ @@ -93,6 +107,7 @@ function resetMoneyRequestInfo(id = '') { participantAccountIDs: [], merchant: CONST.TRANSACTION.DEFAULT_MERCHANT, category: '', + tag: '', created, receiptPath: '', receiptSource: '', @@ -111,6 +126,7 @@ function buildOnyxDataForMoneyRequest( optimisticPersonalDetailListAction, reportPreviewAction, optimisticRecentlyUsedCategories, + optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, ) { @@ -168,6 +184,14 @@ function buildOnyxDataForMoneyRequest( }); } + if (!_.isEmpty(optimisticPolicyRecentlyUsedTags)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`, + value: optimisticPolicyRecentlyUsedTags, + }); + } + if (!_.isEmpty(optimisticPersonalDetailListAction)) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -341,6 +365,7 @@ function buildOnyxDataForMoneyRequest( * @param {Object} [receipt] * @param {String} [existingTransactionID] * @param {String} [category] + * @param {String} [tag] * @param {Boolean} [billable] * @returns {Object} data * @returns {String} data.payerEmail @@ -369,6 +394,7 @@ function getMoneyRequestInformation( receipt = undefined, existingTransactionID = undefined, category = undefined, + tag = undefined, billable = undefined, ) { const payerEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login); @@ -435,6 +461,7 @@ function getMoneyRequestInformation( filename, existingTransactionID, category, + tag, billable, ); @@ -446,6 +473,16 @@ function getMoneyRequestInformation( : []; const optimisticPolicyRecentlyUsedCategories = [category, ...uniquePolicyRecentlyUsedCategories]; + const optimisticPolicyRecentlyUsedTags = {}; + const recentlyUsedPolicyTags = allRecentlyUsedTags[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`]; + + if (recentlyUsedPolicyTags) { + // For now it only uses the first tag of the policy, since multi-tags are not yet supported + const recentlyUsedTagListKey = _.first(_.keys(recentlyUsedPolicyTags)); + const uniquePolicyRecentlyUsedTags = _.filter(recentlyUsedPolicyTags[recentlyUsedTagListKey], (recentlyUsedPolicyTag) => recentlyUsedPolicyTag !== tag); + optimisticPolicyRecentlyUsedTags[recentlyUsedTagListKey] = [tag, ...uniquePolicyRecentlyUsedTags]; + } + // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. @@ -515,6 +552,7 @@ function getMoneyRequestInformation( optimisticPersonalDetailListAction, reportPreviewAction, optimisticPolicyRecentlyUsedCategories, + optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, ); @@ -545,11 +583,12 @@ function getMoneyRequestInformation( * @param {String} created * @param {String} [transactionID] * @param {String} [category] + * @param {String} [tag] * @param {Number} amount * @param {String} currency * @param {String} merchant */ -function createDistanceRequest(report, participant, comment, created, transactionID, category, amount, currency, merchant) { +function createDistanceRequest(report, participant, comment, created, transactionID, category, tag, amount, currency, merchant) { const optimisticReceipt = { source: ReceiptGeneric, state: CONST.IOU.RECEIPT_STATE.OPEN, @@ -567,6 +606,7 @@ function createDistanceRequest(report, participant, comment, created, transactio optimisticReceipt, transactionID, category, + tag, ); API.write( 'CreateDistanceRequest', @@ -582,6 +622,7 @@ function createDistanceRequest(report, participant, comment, created, transactio waypoints: JSON.stringify(TransactionUtils.getValidWaypoints(transaction.comment.waypoints, true)), created, category, + tag, }, onyxData, ); @@ -603,9 +644,24 @@ function createDistanceRequest(report, participant, comment, created, transactio * @param {String} comment * @param {Object} [receipt] * @param {String} [category] + * @param {String} [tag] * @param {Boolean} [billable] */ -function requestMoney(report, amount, currency, created, merchant, payeeEmail, payeeAccountID, participant, comment, receipt = undefined, category = undefined, billable = undefined) { +function requestMoney( + report, + amount, + currency, + created, + merchant, + payeeEmail, + payeeAccountID, + participant, + comment, + receipt = undefined, + category = undefined, + tag = undefined, + billable = undefined, +) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; @@ -622,6 +678,7 @@ function requestMoney(report, amount, currency, created, merchant, payeeEmail, p receipt, undefined, category, + tag, billable, ); @@ -643,6 +700,7 @@ function requestMoney(report, amount, currency, created, merchant, payeeEmail, p reportPreviewReportActionID: reportPreviewAction.reportActionID, receipt, category, + tag, billable, }, onyxData, @@ -928,6 +986,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco oneOnOnePersonalDetailListAction, oneOnOneReportPreviewAction, [], + {}, isNewOneOnOneChatReport, shouldCreateNewOneOnOneIOUReport, ); @@ -1959,6 +2018,17 @@ function resetMoneyRequestCategory() { Onyx.merge(ONYXKEYS.IOU, {category: ''}); } +/* + * @param {String} tag + */ +function setMoneyRequestTag(tag) { + Onyx.merge(ONYXKEYS.IOU, {tag}); +} + +function resetMoneyRequestTag() { + Onyx.merge(ONYXKEYS.IOU, {tag: ''}); +} + /** * @param {Boolean} billable */ @@ -2049,6 +2119,8 @@ export { setMoneyRequestMerchant, setMoneyRequestCategory, resetMoneyRequestCategory, + setMoneyRequestTag, + resetMoneyRequestTag, setMoneyRequestBillable, setMoneyRequestParticipants, setMoneyRequestReceipt, diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index edbae46a3207..cb54aa8e5a7b 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -124,7 +124,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate}) recentReports, personalDetails: newChatPersonalDetails, userToInvite, - } = OptionsListUtils.getNewChatOptions(reports, personalDetails, betas, searchTerm, newSelectedOptions, excludedGroupEmails); + } = OptionsListUtils.getFilteredOptions(reports, personalDetails, betas, searchTerm, newSelectedOptions, excludedGroupEmails); setSelectedOptions(newSelectedOptions); setFilteredRecentReports(recentReports); @@ -159,7 +159,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate}) recentReports, personalDetails: newChatPersonalDetails, userToInvite, - } = OptionsListUtils.getNewChatOptions(reports, personalDetails, betas, searchTerm, selectedOptions, isGroupChat ? excludedGroupEmails : []); + } = OptionsListUtils.getFilteredOptions(reports, personalDetails, betas, searchTerm, selectedOptions, isGroupChat ? excludedGroupEmails : []); setFilteredRecentReports(recentReports); setFilteredPersonalDetails(newChatPersonalDetails); setFilteredUserToInvite(userToInvite); diff --git a/src/pages/iou/MoneyRequestTagPage.js b/src/pages/iou/MoneyRequestTagPage.js index a1795d50df8a..c9b5eb4f8f6f 100644 --- a/src/pages/iou/MoneyRequestTagPage.js +++ b/src/pages/iou/MoneyRequestTagPage.js @@ -5,6 +5,7 @@ import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; import compose from '../../libs/compose'; import ROUTES from '../../ROUTES'; +import * as IOU from '../../libs/actions/IOU'; import Navigation from '../../libs/Navigation/Navigation'; import useLocalize from '../../hooks/useLocalize'; import ScreenWrapper from '../../components/ScreenWrapper'; @@ -15,6 +16,7 @@ import tagPropTypes from '../../components/tagPropTypes'; import ONYXKEYS from '../../ONYXKEYS'; import reportPropTypes from '../reportPropTypes'; import styles from '../../styles/styles'; +import {iouPropTypes, iouDefaultProps} from './propTypes'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -40,14 +42,18 @@ const propTypes = { tags: PropTypes.objectOf(tagPropTypes), }), ), + + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + iou: iouPropTypes, }; const defaultProps = { report: {}, policyTags: {}, + iou: iouDefaultProps, }; -function MoneyRequestTagPage({route, report, policyTags}) { +function MoneyRequestTagPage({route, report, policyTags, iou}) { const {translate} = useLocalize(); const iouType = lodashGet(route, 'params.iouType', ''); @@ -55,10 +61,19 @@ function MoneyRequestTagPage({route, report, policyTags}) { // Fetches the first tag list of the policy const tagListKey = _.first(_.keys(policyTags)); const tagList = lodashGet(policyTags, tagListKey, {}); - const tagListName = lodashGet(tagList, 'name', ''); + const tagListName = lodashGet(tagList, 'name', translate('common.tag')); const navigateBack = () => { - Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, lodashGet(report, 'reportID', ''))); + Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, report.reportID)); + }; + + const updateTag = (selectedTag) => { + if (selectedTag.searchText === iou.tag) { + IOU.resetMoneyRequestTag(); + } else { + IOU.setMoneyRequestTag(selectedTag.searchText); + } + navigateBack(); }; return ( @@ -67,15 +82,15 @@ function MoneyRequestTagPage({route, report, policyTags}) { shouldEnableMaxHeight > - {translate('iou.tagSelection', {tagListName} || translate('common.tag'))} + {translate('iou.tagSelection', {tagName: tagListName})} ); @@ -87,16 +102,13 @@ MoneyRequestTagPage.defaultProps = defaultProps; export default compose( withOnyx({ + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID')}`, + }, iou: { key: ONYXKEYS.IOU, }, }), - withOnyx({ - report: { - // Fetch report ID from IOU participants if no report ID is set in route - key: ({route, iou}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '') || lodashGet(iou, 'participants.0.reportID', '')}`, - }, - }), withOnyx({ policyTags: { key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report ? report.policyID : '0'}`, diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 224f661915d8..93ee2c7f8aac 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -141,6 +141,7 @@ function MoneyRequestConfirmPage(props) { trimmedComment, receipt, props.iou.category, + props.iou.tag, props.iou.billable, ); }, @@ -153,6 +154,7 @@ function MoneyRequestConfirmPage(props) { props.currentUserPersonalDetails.login, props.currentUserPersonalDetails.accountID, props.iou.category, + props.iou.tag, props.iou.billable, ], ); @@ -170,12 +172,13 @@ function MoneyRequestConfirmPage(props) { props.iou.created, props.iou.transactionID, props.iou.category, + props.iou.tag, props.iou.amount, props.iou.currency, props.iou.merchant, ); }, - [props.report, props.iou.created, props.iou.transactionID, props.iou.category, props.iou.amount, props.iou.currency, props.iou.merchant], + [props.report, props.iou.created, props.iou.transactionID, props.iou.category, props.iou.tag, props.iou.amount, props.iou.currency, props.iou.merchant], ); const createTransaction = useCallback( diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 4f761e92eaf5..77ead4bf5a85 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -193,7 +193,7 @@ function MoneyRequestParticipantsSelector({ onAddParticipants(newSelectedOptions); - const chatOptions = OptionsListUtils.getNewChatOptions( + const chatOptions = OptionsListUtils.getFilteredOptions( reports, personalDetails, betas, @@ -228,7 +228,7 @@ function MoneyRequestParticipantsSelector({ const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); useEffect(() => { - const chatOptions = OptionsListUtils.getNewChatOptions( + const chatOptions = OptionsListUtils.getFilteredOptions( reports, personalDetails, betas, diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js index c4dd2dee2d8c..2e7ddea1926d 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.js +++ b/src/pages/tasks/TaskAssigneeSelectorModal.js @@ -83,7 +83,7 @@ function TaskAssigneeSelectorModal(props) { const optionRef = useRef(); const updateOptions = useCallback(() => { - const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getNewChatOptions( + const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( props.reports, props.personalDetails, props.betas, @@ -96,6 +96,9 @@ function TaskAssigneeSelectorModal(props) { {}, [], false, + {}, + [], + false, ); setHeaderMessage(OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), searchValue)); diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 9f99838c6917..637dc1e18376 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -320,12 +320,12 @@ describe('OptionsListUtils', () => { }); }); - it('getNewChatOptions()', () => { + it('getFilteredOptions()', () => { // maxRecentReportsToShow in src/libs/OptionsListUtils.js const MAX_RECENT_REPORTS = 5; - // When we call getNewChatOptions() with no search value - let results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], ''); + // When we call getFilteredOptions() with no search value + let results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], ''); // We should expect maximimum of 5 recent reports to be returned expect(results.recentReports.length).toBe(MAX_RECENT_REPORTS); @@ -345,7 +345,7 @@ describe('OptionsListUtils', () => { expect(personalDetailWithExistingReport.reportID).toBe(2); // When we only pass personal details - results = OptionsListUtils.getNewChatOptions([], PERSONAL_DETAILS, [], ''); + results = OptionsListUtils.getFilteredOptions([], PERSONAL_DETAILS, [], ''); // We should expect personal details sorted alphabetically expect(results.personalDetails[0].text).toBe('Black Panther'); @@ -354,13 +354,13 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[3].text).toBe('Invisible Woman'); // When we provide a search value that does not match any personal details - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'magneto'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'magneto'); // Then no options will be returned expect(results.personalDetails.length).toBe(0); // When we provide a search value that matches an email - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'peterparker@expensify.com'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'peterparker@expensify.com'); // Then one recentReports will be returned and it will be the correct option // personalDetails should be empty array @@ -369,7 +369,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails.length).toBe(0); // When we provide a search value that matches a partial display name or email - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '.com'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '.com'); // Then several options will be returned and they will be each have the search string in their email or name // even though the currently logged in user matches they should not show. @@ -382,7 +382,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports[2].text).toBe('Black Panther'); // Test for Concierge's existence in chat options - results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE); + results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE); // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) @@ -390,30 +390,30 @@ describe('OptionsListUtils', () => { expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results - results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); + results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); // All the personalDetails should be returned minus the currently logged in user and Concierge expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CONCIERGE) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results - results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); + results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); // All the personalDetails should be returned minus the currently logged in user and Concierge expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_CHRONOS) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results - results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); + results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); // All the personalDetails should be returned minus the currently logged in user and Concierge expect(results.personalDetails.length).toBe(_.size(PERSONAL_DETAILS_WITH_RECEIPTS) - 2 - MAX_RECENT_REPORTS); expect(results.personalDetails).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'receipts@expensify.com'})])); }); - it('getNewChatOptions() for group Chat', () => { - // When we call getNewChatOptions() with no search value - let results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], ''); + it('getFilteredOptions() for group Chat', () => { + // When we call getFilteredOptions() with no search value + let results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], ''); // Then we should expect only a maxmimum of 5 recent reports to be returned expect(results.recentReports.length).toBe(5); @@ -434,7 +434,7 @@ describe('OptionsListUtils', () => { expect(personalDetailsOverlapWithReports).toBe(false); // When we search for an option that is only in a personalDetail with no existing report - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'hulk'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'hulk'); // Then reports should return no results expect(results.recentReports.length).toBe(0); @@ -444,7 +444,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[0].login).toBe('brucebanner@expensify.com'); // When we search for an option that matches things in both personalDetails and reports - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '.com'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '.com'); // Then all single participant reports that match will show up in the recentReports array, Recently used contact should be at the top expect(results.recentReports.length).toBe(5); @@ -454,16 +454,16 @@ describe('OptionsListUtils', () => { expect(results.personalDetails.length).toBe(4); expect(results.personalDetails[0].login).toBe('natasharomanoff@expensify.com'); - // When we provide no selected options to getNewChatOptions() - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '', []); + // When we provide no selected options to getFilteredOptions() + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', []); // Then one of our older report options (not in our five most recent) should appear in the personalDetails // but not in recentReports expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); expect(_.every(results.personalDetails, (option) => option.login !== 'peterparker@expensify.com')).toBe(false); - // When we provide a "selected" option to getNewChatOptions() - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '', [{login: 'peterparker@expensify.com'}]); + // When we provide a "selected" option to getFilteredOptions() + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '', [{login: 'peterparker@expensify.com'}]); // Then the option should not appear anywhere in either list expect(_.every(results.recentReports, (option) => option.login !== 'peterparker@expensify.com')).toBe(true); @@ -471,7 +471,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is not a potential email or phone - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify'); // Then we should have no options or personal details at all and also that there is no userToInvite expect(results.recentReports.length).toBe(0); @@ -480,7 +480,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential email - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify.com'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'marc@expensify.com'); // Then we should have no options or personal details at all but there should be a userToInvite expect(results.recentReports.length).toBe(0); @@ -488,7 +488,7 @@ describe('OptionsListUtils', () => { expect(results.userToInvite).not.toBe(null); // When we add a search term with a period, with options for it that don't contain the period - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], 'peter.parker@expensify.com'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], 'peter.parker@expensify.com'); // Then we should have no options at all but there should be a userToInvite expect(results.recentReports.length).toBe(0); @@ -496,7 +496,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number without country code added - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '5005550006'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '5005550006'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -507,7 +507,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with country code added - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '+15005550006'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '+15005550006'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -518,7 +518,7 @@ describe('OptionsListUtils', () => { // When we add a search term for which no options exist and the searchValue itself // is a potential phone number with special characters added - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '+1 (800)324-3233'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '+1 (800)324-3233'); // Then we should have no options or personal details at all but there should be a userToInvite and the login // should have the country code included @@ -528,7 +528,7 @@ describe('OptionsListUtils', () => { expect(results.userToInvite.login).toBe('+18003243233'); // When we use a search term for contact number that contains alphabet characters - results = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa'); + results = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], '998243aaaa'); // Then we shouldn't have any results or user to invite expect(results.recentReports.length).toBe(0); @@ -536,7 +536,7 @@ describe('OptionsListUtils', () => { expect(results.userToInvite).toBe(null); // Test Concierge's existence in new group options - results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE); + results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE); // Concierge is included in the results by default. We should expect all the personalDetails to show // (minus the 5 that are already showing and the currently logged in user) @@ -544,7 +544,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports).toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Concierge from the results - results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); + results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CONCIERGE, PERSONAL_DETAILS_WITH_CONCIERGE, [], '', [], [CONST.EMAIL.CONCIERGE]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) @@ -553,7 +553,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'concierge@expensify.com'})])); // Test by excluding Chronos from the results - results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); + results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_CHRONOS, PERSONAL_DETAILS_WITH_CHRONOS, [], '', [], [CONST.EMAIL.CHRONOS]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) @@ -562,7 +562,7 @@ describe('OptionsListUtils', () => { expect(results.recentReports).not.toEqual(expect.arrayContaining([expect.objectContaining({login: 'chronos@expensify.com'})])); // Test by excluding Receipts from the results - results = OptionsListUtils.getNewChatOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); + results = OptionsListUtils.getFilteredOptions(REPORTS_WITH_RECEIPTS, PERSONAL_DETAILS_WITH_RECEIPTS, [], '', [], [CONST.EMAIL.RECEIPTS]); // We should expect all the personalDetails to show (minus the 5 that are already showing, // the currently logged in user and Concierge) @@ -652,7 +652,7 @@ describe('OptionsListUtils', () => { expect(results.personalDetails[0].text).toBe('Spider-Man'); }); - it('getNewChatOptions() for categories', () => { + it('getFilteredOptions() for categories', () => { const search = 'Food'; const emptySearch = ''; const wrongSearch = 'bla bla'; @@ -1001,16 +1001,16 @@ describe('OptionsListUtils', () => { }, ]; - const smallResult = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, true, smallCategoriesList); + const smallResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, true, smallCategoriesList); expect(smallResult.categoryOptions).toStrictEqual(smallResultList); - const smallSearchResult = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, true, smallCategoriesList); + const smallSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, true, smallCategoriesList); expect(smallSearchResult.categoryOptions).toStrictEqual(smallSearchResultList); - const smallWrongSearchResult = OptionsListUtils.getNewChatOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, true, smallCategoriesList); + const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, true, smallCategoriesList); expect(smallWrongSearchResult.categoryOptions).toStrictEqual(smallWrongSearchResultList); - const largeResult = OptionsListUtils.getNewChatOptions( + const largeResult = OptionsListUtils.getFilteredOptions( REPORTS, PERSONAL_DETAILS, [], @@ -1025,7 +1025,7 @@ describe('OptionsListUtils', () => { ); expect(largeResult.categoryOptions).toStrictEqual(largeResultList); - const largeSearchResult = OptionsListUtils.getNewChatOptions( + const largeSearchResult = OptionsListUtils.getFilteredOptions( REPORTS, PERSONAL_DETAILS, [], @@ -1040,7 +1040,7 @@ describe('OptionsListUtils', () => { ); expect(largeSearchResult.categoryOptions).toStrictEqual(largeSearchResultList); - const largeWrongSearchResult = OptionsListUtils.getNewChatOptions( + const largeWrongSearchResult = OptionsListUtils.getFilteredOptions( REPORTS, PERSONAL_DETAILS, [], @@ -1056,6 +1056,317 @@ describe('OptionsListUtils', () => { expect(largeWrongSearchResult.categoryOptions).toStrictEqual(largeWrongSearchResultList); }); + it('getFilteredOptions() for tags', () => { + const search = 'ing'; + const emptySearch = ''; + const wrongSearch = 'bla bla'; + const recentlyUsedTags = ['Engineering', 'HR']; + + const selectedOptions = [ + { + name: 'Medical', + }, + ]; + const smallTagsList = { + Engineering: { + enabled: false, + name: 'Engineering', + }, + Medical: { + enabled: true, + name: 'Medical', + }, + Accounting: { + enabled: true, + name: 'Accounting', + }, + HR: { + enabled: true, + name: 'HR', + }, + }; + const smallResultList = [ + { + title: '', + shouldShow: false, + indexOffset: 0, + data: [ + { + text: 'Medical', + keyForList: 'Medical', + searchText: 'Medical', + tooltipText: 'Medical', + isDisabled: false, + }, + { + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, + }, + { + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', + isDisabled: false, + }, + ], + }, + ]; + const smallSearchResultList = [ + { + title: '', + shouldShow: false, + indexOffset: 0, + data: [ + { + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, + }, + ], + }, + ]; + const smallWrongSearchResultList = [ + { + title: '', + shouldShow: false, + indexOffset: 0, + data: [], + }, + ]; + const largeTagsList = { + Engineering: { + enabled: false, + name: 'Engineering', + }, + Medical: { + enabled: true, + name: 'Medical', + }, + Accounting: { + enabled: true, + name: 'Accounting', + }, + HR: { + enabled: true, + name: 'HR', + }, + Food: { + enabled: true, + name: 'Food', + }, + Traveling: { + enabled: false, + name: 'Traveling', + }, + Cleaning: { + enabled: true, + name: 'Cleaning', + }, + Software: { + enabled: true, + name: 'Software', + }, + OfficeSupplies: { + enabled: false, + name: 'Office Supplies', + }, + Taxes: { + enabled: true, + name: 'Taxes', + }, + Benefits: { + enabled: true, + name: 'Benefits', + }, + }; + const largeResultList = [ + { + title: '', + shouldShow: false, + indexOffset: 0, + data: [ + { + text: 'Medical', + keyForList: 'Medical', + searchText: 'Medical', + tooltipText: 'Medical', + isDisabled: false, + }, + ], + }, + { + title: 'Recent', + shouldShow: true, + indexOffset: 1, + data: [ + { + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', + isDisabled: false, + }, + ], + }, + { + title: 'All', + shouldShow: true, + indexOffset: 2, + data: [ + { + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, + }, + { + text: 'HR', + keyForList: 'HR', + searchText: 'HR', + tooltipText: 'HR', + isDisabled: false, + }, + { + text: 'Food', + keyForList: 'Food', + searchText: 'Food', + tooltipText: 'Food', + isDisabled: false, + }, + { + text: 'Cleaning', + keyForList: 'Cleaning', + searchText: 'Cleaning', + tooltipText: 'Cleaning', + isDisabled: false, + }, + { + text: 'Software', + keyForList: 'Software', + searchText: 'Software', + tooltipText: 'Software', + isDisabled: false, + }, + { + text: 'Taxes', + keyForList: 'Taxes', + searchText: 'Taxes', + tooltipText: 'Taxes', + isDisabled: false, + }, + { + text: 'Benefits', + keyForList: 'Benefits', + searchText: 'Benefits', + tooltipText: 'Benefits', + isDisabled: false, + }, + ], + }, + ]; + const largeSearchResultList = [ + { + title: '', + shouldShow: false, + indexOffset: 0, + data: [ + { + text: 'Accounting', + keyForList: 'Accounting', + searchText: 'Accounting', + tooltipText: 'Accounting', + isDisabled: false, + }, + { + text: 'Cleaning', + keyForList: 'Cleaning', + searchText: 'Cleaning', + tooltipText: 'Cleaning', + isDisabled: false, + }, + ], + }, + ]; + const largeWrongSearchResultList = [ + { + title: '', + shouldShow: false, + indexOffset: 0, + data: [], + }, + ]; + + const smallResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], emptySearch, [], [], false, false, false, {}, [], true, smallTagsList); + expect(smallResult.tagOptions).toStrictEqual(smallResultList); + + const smallSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], search, [], [], false, false, false, {}, [], true, smallTagsList); + expect(smallSearchResult.tagOptions).toStrictEqual(smallSearchResultList); + + const smallWrongSearchResult = OptionsListUtils.getFilteredOptions(REPORTS, PERSONAL_DETAILS, [], wrongSearch, [], [], false, false, false, {}, [], true, smallTagsList); + expect(smallWrongSearchResult.tagOptions).toStrictEqual(smallWrongSearchResultList); + + const largeResult = OptionsListUtils.getFilteredOptions( + REPORTS, + PERSONAL_DETAILS, + [], + emptySearch, + selectedOptions, + [], + false, + false, + false, + {}, + [], + true, + largeTagsList, + recentlyUsedTags, + ); + expect(largeResult.tagOptions).toStrictEqual(largeResultList); + + const largeSearchResult = OptionsListUtils.getFilteredOptions( + REPORTS, + PERSONAL_DETAILS, + [], + search, + selectedOptions, + [], + false, + false, + false, + {}, + [], + true, + largeTagsList, + recentlyUsedTags, + ); + expect(largeSearchResult.tagOptions).toStrictEqual(largeSearchResultList); + + const largeWrongSearchResult = OptionsListUtils.getFilteredOptions( + REPORTS, + PERSONAL_DETAILS, + [], + wrongSearch, + selectedOptions, + [], + false, + false, + false, + {}, + [], + true, + largeTagsList, + recentlyUsedTags, + ); + expect(largeWrongSearchResult.tagOptions).toStrictEqual(largeWrongSearchResultList); + }); + it('getCategoryOptionTree()', () => { const categories = { Taxi: {