-
Notifications
You must be signed in to change notification settings - Fork 3.9k
#26126: Tag menu item and Tag picker (1st PR) #26954
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
002bcf0
c0e2352
6e6ec88
412e191
e2cf2b5
7aca47c
601231f
51b6979
92cf2a6
68a3893
b4f8adf
1112ae6
feb8020
40bd151
1a78dfd
b7fd431
65a6b48
5c9738e
016ad53
9b7d7c1
77b853d
c42584c
3f40a65
2a0d68b
40eabd8
02d1ddd
298193d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import lodashGet from 'lodash/get'; | |
| import styles from '../styles/styles'; | ||
| import * as ReportUtils from '../libs/ReportUtils'; | ||
| import * as OptionsListUtils from '../libs/OptionsListUtils'; | ||
| import Permissions from '../libs/Permissions'; | ||
| import OptionsSelector from './OptionsSelector'; | ||
| import ONYXKEYS from '../ONYXKEYS'; | ||
| import compose from '../libs/compose'; | ||
|
|
@@ -29,11 +30,11 @@ import Image from './Image'; | |
| import useLocalize from '../hooks/useLocalize'; | ||
| import * as ReceiptUtils from '../libs/ReceiptUtils'; | ||
| import categoryPropTypes from './categoryPropTypes'; | ||
| import tagPropTypes from './tagPropTypes'; | ||
| import ConfirmedRoute from './ConfirmedRoute'; | ||
| import transactionPropTypes from './transactionPropTypes'; | ||
| import DistanceRequestUtils from '../libs/DistanceRequestUtils'; | ||
| import * as IOU from '../libs/actions/IOU'; | ||
| import Permissions from '../libs/Permissions'; | ||
|
|
||
| const propTypes = { | ||
| /** Callback to inform parent modal of success */ | ||
|
|
@@ -69,6 +70,9 @@ const propTypes = { | |
| /** IOU Category */ | ||
| iouCategory: PropTypes.string, | ||
|
|
||
| /** IOU Tag */ | ||
| iouTag: PropTypes.string, | ||
|
|
||
| /** Selected participants from MoneyRequestModal with login / accountID */ | ||
| selectedParticipants: PropTypes.arrayOf(optionPropTypes).isRequired, | ||
|
|
||
|
|
@@ -109,10 +113,6 @@ const propTypes = { | |
| /** List styles for OptionsSelector */ | ||
| listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), | ||
|
|
||
| /* Onyx Props */ | ||
| /** Collection of categories attached to a policy */ | ||
| policyCategories: PropTypes.objectOf(categoryPropTypes), | ||
|
|
||
| /** ID of the transaction that represents the money request */ | ||
| transactionID: PropTypes.string, | ||
|
|
||
|
|
@@ -133,6 +133,19 @@ const propTypes = { | |
|
|
||
| /** Whether the money request is a distance request */ | ||
| isDistanceRequest: PropTypes.bool, | ||
|
|
||
| /* Onyx Props */ | ||
| /** Collection of categories attached to a policy */ | ||
| policyCategories: PropTypes.objectOf(categoryPropTypes), | ||
|
|
||
| /** Collection of tags attached to a policy */ | ||
| policyTags: PropTypes.objectOf( | ||
| PropTypes.shape({ | ||
| name: PropTypes.string, | ||
| required: PropTypes.bool, | ||
| tags: PropTypes.objectOf(tagPropTypes), | ||
| }), | ||
| ), | ||
| }; | ||
|
|
||
| const defaultProps = { | ||
|
|
@@ -141,6 +154,7 @@ const defaultProps = { | |
| onSelectParticipant: () => {}, | ||
| iouType: CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, | ||
| iouCategory: '', | ||
| iouTag: '', | ||
| payeePersonalDetails: null, | ||
| canModifyParticipants: false, | ||
| isReadOnly: false, | ||
|
|
@@ -156,6 +170,7 @@ const defaultProps = { | |
| receiptSource: '', | ||
| listStyles: [], | ||
| policyCategories: {}, | ||
| policyTags: {}, | ||
| transactionID: '', | ||
| transaction: {}, | ||
| mileageRate: {unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate: 0, currency: 'USD'}, | ||
|
|
@@ -178,6 +193,12 @@ function MoneyRequestConfirmationList(props) { | |
| const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0; | ||
| const shouldCategoryBeEditable = !_.isEmpty(props.policyCategories) && Permissions.canUseCategories(props.betas); | ||
|
|
||
| // Fetches the first tag list of the policy | ||
| const tagListKey = _.first(_.keys(props.policyTags)); | ||
| const tagList = lodashGet(props.policyTags, [tagListKey, 'tags'], []); | ||
| const tagListName = lodashGet(props.policyTags, [tagListKey, 'name'], ''); | ||
| const canUseTags = Permissions.canUseTags(props.betas); | ||
|
|
||
| const formattedAmount = CurrencyUtils.convertToDisplayString( | ||
| shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, | ||
| props.isDistanceRequest ? currency : props.iouCurrencyCode, | ||
|
|
@@ -499,6 +520,16 @@ function MoneyRequestConfirmationList(props) { | |
| disabled={didConfirm || props.isReadOnly} | ||
| /> | ||
| )} | ||
| {canUseTags && !!tagList && ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just noticed this line is incorrect, we should use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yap! |
||
| <MenuItemWithTopDescription | ||
| shouldShowRightIcon={!props.isReadOnly} | ||
| title={props.iouTag} | ||
| description={tagListName || translate('common.tag')} | ||
| onPress={() => Navigation.navigate(ROUTES.getMoneyRequestTagRoute(props.iouType, props.reportID))} | ||
| style={[styles.moneyRequestMenuItem, styles.mb2]} | ||
| disabled={didConfirm || props.isReadOnly} | ||
| /> | ||
| )} | ||
| </> | ||
| )} | ||
| </OptionsSelector> | ||
|
|
@@ -520,6 +551,9 @@ export default compose( | |
| policyCategories: { | ||
| key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, | ||
| }, | ||
| policyTags: { | ||
| key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, | ||
| }, | ||
| mileageRate: { | ||
| key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, | ||
| selector: DistanceRequestUtils.getDefaultMileageRate, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import React, {useMemo} from 'react'; | ||
| import _ from 'underscore'; | ||
| import lodashGet from 'lodash/get'; | ||
| import {withOnyx} from 'react-native-onyx'; | ||
| import ONYXKEYS from '../../ONYXKEYS'; | ||
| import styles from '../../styles/styles'; | ||
| import Navigation from '../../libs/Navigation/Navigation'; | ||
| import ROUTES from '../../ROUTES'; | ||
| import useLocalize from '../../hooks/useLocalize'; | ||
| import * as OptionsListUtils from '../../libs/OptionsListUtils'; | ||
| import OptionsSelector from '../OptionsSelector'; | ||
| import {propTypes, defaultProps} from './tagPickerPropTypes'; | ||
|
|
||
| function TagPicker({policyTags, reportID, tag, iouType, iou}) { | ||
| const {translate} = useLocalize(); | ||
|
|
||
| const selectedOptions = useMemo(() => { | ||
|
BeeMargarida marked this conversation as resolved.
|
||
| if (!iou.tag) { | ||
| return []; | ||
| } | ||
|
|
||
| return [ | ||
| { | ||
| name: iou.tag, | ||
| enabled: true, | ||
| }, | ||
| ]; | ||
| }, [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(); | ||
|
|
||
| return [ | ||
| { | ||
| data: tagList, | ||
| }, | ||
| ]; | ||
| }, [policyTags, tag]); | ||
|
|
||
| const headerMessage = OptionsListUtils.getHeaderMessage(lodashGet(sections, '[0].data.length', 0) > 0, false, ''); | ||
|
|
||
| const navigateBack = () => { | ||
| Navigation.goBack(ROUTES.getMoneyRequestConfirmationRoute(iouType, reportID)); | ||
| }; | ||
|
|
||
| const updateTag = () => { | ||
| // TODO: add logic to save the selected tag | ||
| navigateBack(); | ||
| }; | ||
|
|
||
| return ( | ||
| <OptionsSelector | ||
|
BeeMargarida marked this conversation as resolved.
|
||
| optionHoveredStyle={styles.hoveredComponentBG} | ||
| sections={sections} | ||
| selectedOptions={selectedOptions} | ||
| headerMessage={headerMessage} | ||
| textInputLabel={translate('common.search')} | ||
| boldStyle | ||
| value="" | ||
| onSelectRow={updateTag} | ||
| shouldShowTextInput={false} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| TagPicker.displayName = 'TagPicker'; | ||
| TagPicker.propTypes = propTypes; | ||
| TagPicker.defaultProps = defaultProps; | ||
|
|
||
| export default withOnyx({ | ||
| policyTags: { | ||
| key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, | ||
| }, | ||
| policyRecentlyUsedTags: { | ||
| key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, | ||
| }, | ||
| iou: { | ||
| key: ONYXKEYS.IOU, | ||
| }, | ||
| })(TagPicker); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| 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 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, | ||
|
|
||
| /* Onyx Props */ | ||
| /** Collection of tags attached to a policy */ | ||
| policyTags: PropTypes.objectOf( | ||
| PropTypes.shape({ | ||
| name: PropTypes.string, | ||
| tags: PropTypes.objectOf(tagPropTypes), | ||
| }), | ||
| ), | ||
|
|
||
| /** 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}; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import PropTypes from 'prop-types'; | ||
|
|
||
| export default PropTypes.shape({ | ||
|
amyevans marked this conversation as resolved.
|
||
| /** Name of a tag */ | ||
| name: PropTypes.string.isRequired, | ||
|
|
||
| /** Flag that determines if a tag is active and able to be selected */ | ||
| enabled: PropTypes.bool.isRequired, | ||
|
BeeMargarida marked this conversation as resolved.
|
||
|
|
||
| /** "General Ledger code" that corresponds to this tag in an accounting system. Similar to an ID. */ | ||
| 'GL Code': PropTypes.string, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -71,6 +71,7 @@ import type { | |
| SetTheRequestParams, | ||
| UpdatedTheRequestParams, | ||
| RemovedTheRequestParams, | ||
| TagSelectionParams, | ||
| TranslationBase, | ||
| } from './types'; | ||
| import * as ReportActionsUtils from '../libs/ReportActionsUtils'; | ||
|
|
@@ -243,6 +244,7 @@ export default { | |
| showMore: 'Show more', | ||
| merchant: 'Merchant', | ||
| category: 'Category', | ||
| tag: 'Tag', | ||
| receipt: 'Receipt', | ||
| replace: 'Replace', | ||
| distance: 'Distance', | ||
|
|
@@ -544,6 +546,7 @@ export default { | |
| `changed the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, | ||
| threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, | ||
| threadSentMoneyReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, | ||
| tagSelection: ({tagName}: TagSelectionParams) => `Select a ${tagName} to add additional organization to your money`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this copy was checked? It does not really read like a proper English sentence. We're going to create an issue to modify it.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's the copy that was in the mocks in the design doc, but yeah I am not sure if it was run through marketing prior to landing in the mocks (I did not run it through after, that much I know 😅). I agree it's a bit awkward wording and could be improved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed here: #38242 |
||
| error: { | ||
| invalidSplit: 'Split amounts do not equal total amount', | ||
| other: 'Unexpected error, please try again later', | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.