diff --git a/src/components/TagPicker/index.js b/src/components/TagPicker/index.tsx similarity index 53% rename from src/components/TagPicker/index.js rename to src/components/TagPicker/index.tsx index 341ea9cddae9..af8acd19e8c4 100644 --- a/src/components/TagPicker/index.js +++ b/src/components/TagPicker/index.tsx @@ -1,7 +1,7 @@ -import lodashGet from 'lodash/get'; import React, {useMemo, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {EdgeInsets} from 'react-native-safe-area-context'; import OptionsSelector from '@components/OptionsSelector'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -10,22 +10,64 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {defaultProps, propTypes} from './tagPickerPropTypes'; +import type {PolicyTag, PolicyTagList, PolicyTags, RecentlyUsedTags} from '@src/types/onyx'; -function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption, insets, onSubmit}) { +type SelectedTagOption = { + name: string; + enabled: boolean; + accountID: number | null; +}; + +type TagPickerOnyxProps = { + /** Collection of tag list on a policy */ + policyTags: OnyxEntry; + + /** List of recently used tags */ + policyRecentlyUsedTags: OnyxEntry; +}; + +type TagPickerProps = TagPickerOnyxProps & { + /** The policyID we are getting tags for */ + // It's used in withOnyx HOC. + // eslint-disable-next-line react/no-unused-prop-types + policyID: string; + + /** The selected tag of the money request */ + selectedTag: string; + + /** The name of tag list we are getting tags for */ + tagListName: string; + + /** Callback to submit the selected tag */ + onSubmit: () => void; + + /** + * Safe area insets required for reflecting the portion of the view, + * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. + */ + insets: EdgeInsets; + + /** Should show the selected option that is disabled? */ + shouldShowDisabledAndSelectedOption?: boolean; + + /** Indicates which tag list index was selected */ + tagListIndex: number; +}; + +function TagPicker({selectedTag, tagListName, policyTags, tagListIndex, policyRecentlyUsedTags, shouldShowDisabledAndSelectedOption = false, insets, onSubmit}: TagPickerProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const [searchValue, setSearchValue] = useState(''); - const policyRecentlyUsedTagsList = lodashGet(policyRecentlyUsedTags, tag, []); - const policyTagList = PolicyUtils.getTagList(policyTags, tagIndex); + const policyRecentlyUsedTagsList = useMemo(() => policyRecentlyUsedTags?.[tagListName] ?? [], [policyRecentlyUsedTags, tagListName]); + const policyTagList = PolicyUtils.getTagList(policyTags, tagListIndex); const policyTagsCount = PolicyUtils.getCountOfEnabledTagsOfList(policyTagList.tags); const isTagsCountBelowThreshold = policyTagsCount < CONST.TAG_LIST_THRESHOLD; const shouldShowTextInput = !isTagsCountBelowThreshold; - const selectedOptions = useMemo(() => { + const selectedOptions: SelectedTagOption[] = useMemo(() => { if (!selectedTag) { return []; } @@ -39,13 +81,13 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa ]; }, [selectedTag]); - const enabledTags = useMemo(() => { + const enabledTags: PolicyTags | Array = useMemo(() => { if (!shouldShowDisabledAndSelectedOption) { return policyTagList.tags; } - const selectedNames = _.map(selectedOptions, (s) => s.name); - const tags = [...selectedOptions, ..._.filter(policyTagList.tags, (policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))]; - return tags; + const selectedNames = selectedOptions.map((s) => s.name); + + return [...selectedOptions, ...Object.values(policyTagList.tags).filter((policyTag) => policyTag.enabled && !selectedNames.includes(policyTag.name))]; }, [selectedOptions, policyTagList, shouldShowDisabledAndSelectedOption]); const sections = useMemo( @@ -53,12 +95,13 @@ function TagPicker({selectedTag, tag, tagIndex, policyTags, policyRecentlyUsedTa [searchValue, enabledTags, selectedOptions, policyRecentlyUsedTagsList], ); - const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList(lodashGet(sections, '[0].data.length', 0) > 0, searchValue); + const headerMessage = OptionsListUtils.getHeaderMessageForNonUserList((sections?.[0]?.data?.length ?? 0) > 0, searchValue); - const selectedOptionKey = lodashGet(_.filter(lodashGet(sections, '[0].data', []), (policyTag) => policyTag.searchText === selectedTag)[0], 'keyForList'); + const selectedOptionKey = sections[0]?.data?.filter((policyTag) => policyTag.searchText === selectedTag)?.[0]?.keyForList; return ( ({ policyTags: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, @@ -92,3 +133,5 @@ export default withOnyx({ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, }, })(TagPicker); + +export type {SelectedTagOption}; diff --git a/src/components/TagPicker/tagPickerPropTypes.js b/src/components/TagPicker/tagPickerPropTypes.js deleted file mode 100644 index cbdc73f5d056..000000000000 --- a/src/components/TagPicker/tagPickerPropTypes.js +++ /dev/null @@ -1,44 +0,0 @@ -import PropTypes from 'prop-types'; -import tagPropTypes from '@components/tagPropTypes'; -import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; - -const propTypes = { - /** 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 index of a tag list */ - tagIndex: PropTypes.number.isRequired, - - /** Callback to submit the selected tag */ - onSubmit: PropTypes.func.isRequired, - - /** - * Safe area insets required for reflecting the portion of the view, - * that is not covered by navigation bars, tab bars, toolbars, and other ancestor views. - */ - insets: safeAreaInsetPropTypes.isRequired, - - /* Onyx Props */ - /** Collection of tags attached to a policy */ - policyTags: tagPropTypes, - - /** List of recently used tags */ - policyRecentlyUsedTags: PropTypes.objectOf(PropTypes.arrayOf(PropTypes.string)), - - /** Should show the selected option that is disabled? */ - shouldShowDisabledAndSelectedOption: PropTypes.bool, -}; - -const defaultProps = { - policyTags: {}, - policyRecentlyUsedTags: {}, - shouldShowDisabledAndSelectedOption: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 46e217ba20b1..372881b41a86 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -7,6 +7,7 @@ import lodashSet from 'lodash/set'; import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {SelectedTagOption} from '@components/TagPicker'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -19,6 +20,7 @@ import type { PolicyCategories, PolicyTag, PolicyTagList, + PolicyTags, Report, ReportAction, ReportActions, @@ -54,12 +56,6 @@ import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -type Tag = { - enabled: boolean; - name: string; - accountID: number | null; -}; - type Option = Partial; /** @@ -131,7 +127,7 @@ type GetOptionsConfig = { categories?: PolicyCategories; recentlyUsedCategories?: string[]; includeTags?: boolean; - tags?: Record; + tags?: PolicyTags | Array; recentlyUsedTags?: string[]; canInviteUser?: boolean; includeSelectedOptions?: boolean; @@ -914,7 +910,7 @@ function sortCategories(categories: Record): Category[] { /** * Sorts tags alphabetically by name. */ -function sortTags(tags: Record | Tag[]) { +function sortTags(tags: Record | Array) { let sortedTags; if (Array.isArray(tags)) { @@ -1095,7 +1091,7 @@ function getCategoryListSections( * * @param tags - an initial tag array */ -function getTagsOptions(tags: Category[]): Option[] { +function getTagsOptions(tags: Array>): Option[] { return tags.map((tag) => { // This is to remove unnecessary escaping backslash in tag name sent from backend. const cleanedName = PolicyUtils.getCleanedTagName(tag.name); @@ -1112,7 +1108,13 @@ function getTagsOptions(tags: Category[]): Option[] { /** * Build the section list for tags */ -function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOptions: Category[], searchInputValue: string, maxRecentReportsToShow: number) { +function getTagListSections( + tags: Array, + recentlyUsedTags: string[], + selectedOptions: SelectedTagOption[], + searchInputValue: string, + maxRecentReportsToShow: number, +) { const tagSections = []; const sortedTags = sortTags(tags); const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); @@ -1424,7 +1426,7 @@ function getOptions( } if (includeTags) { - const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as Category[], searchInputValue, maxRecentReportsToShow); + const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions as SelectedTagOption[], searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -1851,7 +1853,7 @@ function getFilteredOptions( categories: PolicyCategories = {}, recentlyUsedCategories: string[] = [], includeTags = false, - tags: Record = {}, + tags: PolicyTags | Array = {}, recentlyUsedTags: string[] = [], canInviteUser = true, includeSelectedOptions = false, diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 7d10e0e55e79..eecffd81d88b 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -37,7 +37,7 @@ const propTypes = { /** reportID for the "transaction thread" */ threadReportID: PropTypes.string, - /** The index of a tag list */ + /** Indicates which tag list index was selected */ tagIndex: PropTypes.string, }), }).isRequired, @@ -78,10 +78,10 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency; const fieldToEdit = lodashGet(route, ['params', 'field'], ''); - const tagIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined)); + const tagListIndex = Number(lodashGet(route, ['params', 'tagIndex'], undefined)); - const tag = TransactionUtils.getTag(transaction, tagIndex); - const policyTagListName = PolicyUtils.getTagListName(policyTags, tagIndex); + const tag = TransactionUtils.getTag(transaction, tagListIndex); + const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for verifying that the current report is a sub-report of a workspace chat @@ -129,14 +129,14 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p IOU.updateMoneyRequestTag( transaction.transactionID, report.reportID, - IOUUtils.insertTagIntoTransactionTagsString(transactionTag, updatedTag, tagIndex), + IOUUtils.insertTagIntoTransactionTagsString(transactionTag, updatedTag, tagListIndex), policy, policyTags, policyCategories, ); Navigation.dismissModal(); }, - [tag, transaction.transactionID, report.reportID, transactionTag, tagIndex, policy, policyTags, policyCategories], + [tag, transaction.transactionID, report.reportID, transactionTag, tagListIndex, policy, policyTags, policyCategories], ); if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT) { @@ -159,7 +159,7 @@ function EditRequestPage({report, route, policy, policyCategories, policyTags, p diff --git a/src/pages/EditRequestTagPage.js b/src/pages/EditRequestTagPage.js index b64cb925a213..1aead9ee1f6e 100644 --- a/src/pages/EditRequestTagPage.js +++ b/src/pages/EditRequestTagPage.js @@ -15,21 +15,21 @@ const propTypes = { /** The policyID we are getting tags for */ policyID: PropTypes.string.isRequired, - /** The tag name to which the default tag belongs to */ - tagName: PropTypes.string, + /** The tag list name to which the default tag belongs to */ + tagListName: PropTypes.string, - /** The index of a tag list */ - tagIndex: PropTypes.number.isRequired, + /** Indicates which tag list index was selected */ + tagListIndex: PropTypes.number.isRequired, /** Callback to fire when the Save button is pressed */ onSubmit: PropTypes.func.isRequired, }; const defaultProps = { - tagName: '', + tagListName: '', }; -function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit}) { +function EditRequestTagPage({defaultTag, policyID, tagListName, tagListIndex, onSubmit}) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -46,14 +46,14 @@ function EditRequestTagPage({defaultTag, policyID, tagName, tagIndex, onSubmit}) {({insets}) => ( <> {translate('iou.tagSelection')} { setDraftSplitTransaction({tag: transactionChanges.tag.trim()}); }} diff --git a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js index dbcf83bda62a..8b191fa0b58e 100644 --- a/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js +++ b/src/pages/iou/request/step/IOURequestStepRoutePropTypes.js @@ -22,5 +22,8 @@ export default PropTypes.shape({ /** A path to go to when the user presses the back button */ backTo: PropTypes.string, + + /** Indicates which tag list index was selected */ + tagIndex: PropTypes.string, }), }); diff --git a/src/pages/iou/request/step/IOURequestStepTag.js b/src/pages/iou/request/step/IOURequestStepTag.js index 79ed26b76b19..ed55628ecaa9 100644 --- a/src/pages/iou/request/step/IOURequestStepTag.js +++ b/src/pages/iou/request/step/IOURequestStepTag.js @@ -91,15 +91,15 @@ function IOURequestStepTag({ const styles = useThemeStyles(); const {translate} = useLocalize(); - const tagIndex = Number(rawTagIndex); - const policyTagListName = PolicyUtils.getTagListName(policyTags, tagIndex); + const tagListIndex = Number(rawTagIndex); + const policyTagListName = PolicyUtils.getTagListName(policyTags, tagListIndex); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; const isEditingSplitBill = isEditing && isSplitBill; const currentTransaction = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction; const transactionTag = TransactionUtils.getTag(currentTransaction); - const tag = TransactionUtils.getTag(currentTransaction, tagIndex); + const tag = TransactionUtils.getTag(currentTransaction, tagListIndex); const reportAction = reportActions[report.parentReportActionID || reportActionID]; const canEditSplitBill = isSplitBill && reportAction && session.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); @@ -119,7 +119,7 @@ function IOURequestStepTag({ */ const updateTag = (selectedTag) => { const isSelectedTag = selectedTag.searchText === tag; - const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTag.searchText, tagIndex); + const updatedTag = IOUUtils.insertTagIntoTransactionTagsString(transactionTag, isSelectedTag ? '' : selectedTag.searchText, tagListIndex); if (isEditingSplitBill) { IOU.setDraftSplitTransaction(transactionID, {tag: updatedTag}); navigateBack(); @@ -147,8 +147,8 @@ function IOURequestStepTag({ {translate('iou.tagSelection')} = Record< /** Flag that determines if tags are required */ required: boolean; - /** Nested tags */ + /** List of tags */ tags: PolicyTags; /** Index by which the tag appears in the hierarchy of tags */