diff --git a/android/app/build.gradle b/android/app/build.gradle index 25ff4448ecb8..8bf0bf928e4e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042700 - versionName "1.4.27-0" + versionCode 1001042701 + versionName "1.4.27-1" } flavorDimensions "default" diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index 73b6c9106e4e..fb84e3484598 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -65,7 +65,7 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s ### How This Works 1. On the day of your first card settlement, we'll create the Expensify Card Liability account in your QuickBooks Online general ledger. If you've opted for Daily Settlement, we'll also create an Expensify Clearing Account. 2. During your QuickBooks Online auto-sync on that same day, if there are unsettled transactions, we'll generate a journal entry totaling all posted transactions since the last settlement. This entry will credit the selected bank account and debit the new Expensify Clearing Account (for Daily Settlement) or the Expensify Liability Account (for Monthly Settlement). -3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual credit card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories. +3. Once the transactions are posted and the expense report is approved in Expensify, the report will be exported to QuickBooks Online with each line as individual card expenses. For Daily Settlement, an additional journal entry will credit the Expensify Clearing Account and debit the Expensify Card Liability Account. For Monthly Settlement, the journal entry will credit the Liability account directly and debit the appropriate expense categories. ### Example - We have card transactions for the day totaling $100, so we create the following journal entry upon sync: diff --git a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md index 2dec9ae752b8..c5e8da3fae6a 100644 --- a/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md +++ b/docs/articles/expensify-classic/getting-started/playbooks/Expensify-Playbook-For-US-Based-VC-Backed-Startups.md @@ -80,7 +80,7 @@ For an efficiency-focused company, we recommend setting up [Scheduled Submit](ht 4. You’ll notice *Scheduled Submit* is located directly under *Report Basics* 5. Choose *Daily* -Between Expensify's SmartScan technology, direct corporate card feed import, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Card or scan their receipt. +Between Expensify's SmartScan technology, direct corporate card feed import, automatic categorization, and [DoubleCheck](https://community.expensify.com/discussion/5738/deep-dive-how-does-concierge-receipt-audit-work) features, your employees shouldn't need to do anything more than swipe their Expensify Visa® Commercial Card or scan their receipt. Scheduled Submit will ensure all expenses are submitted automatically. Any expenses that do not fall within the rules you’ve set up for your policy will be escalated to you for manual review. @@ -155,7 +155,7 @@ The Expensify Card has many benefits for your company. Two in particular are wor ### If you don't have a corporate card, use the Expensify Card Expensify provides a corporate card with the following features: -- Up to 2% cash back (within the US) +- Up to 2% cash back (Applies to USD purchases only) - [SmartLimits](https://community.expensify.com/discussion/4851/deep-dive-what-are-unapproved-expense-limits#latest) - Unlimited Virtual Cards - single-purpose cards with a fixed or monthly limit for specific company purchases - A stable, unbreakable connection (third-party bank feeds can run into connectivity issues) diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index ca8218d53d87..73ddb84be0e3 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.27.0 + 1.4.27.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 4a261dccc11e..b697d413129d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.27.0 + 1.4.27.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 0ba053ad0026..0a7aab285d15 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.27 CFBundleVersion - 1.4.27.0 + 1.4.27.1 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 1dcdff30b536..c3a48044479a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.27-0", + "version": "1.4.27-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.27-0", + "version": "1.4.27-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5664d2326a57..da993f136512 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.27-0", + "version": "1.4.27-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index 173d9203b419..0e85ff3501cf 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1368,10 +1368,15 @@ const CONST = { TRIP: 'trip', MANUAL: 'manual', }, + AUTO_REPORTING_OFFSET: { + LAST_BUSINESS_DAY_OF_MONTH: 'lastBusinessDayOfMonth', + LAST_DAY_OF_MONTH: 'lastDayOfMonth', + }, ROOM_PREFIX: '#', CUSTOM_UNIT_RATE_BASE_OFFSET: 100, OWNER_EMAIL_FAKE: '_FAKE_', OWNER_ACCOUNT_ID_FAKE: 0, + ID_FAKE: '_FAKE_', }, CUSTOM_UNITS: { @@ -3142,7 +3147,8 @@ const CONST = { }, /** - * Constants for maxToRenderPerBatch parameter that is used for FlatList or SectionList. This controls the amount of items rendered per batch, which is the next chunk of items rendered on every scroll. + * Constants for maxToRenderPerBatch parameter that is used for FlatList or SectionList. This controls the amount of items rendered per batch, which is the next chunk of items + * rendered on every scroll. */ MAX_TO_RENDER_PER_BATCH: { DEFAULT: 5, @@ -3154,6 +3160,11 @@ const CONST = { RBR: 'RBR', }, + /** + * Constants for types of violations. + * Defined here because they need to be referenced by the type system to generate the + * ViolationNames type. + */ VIOLATIONS: { ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired', AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5b2171af73f2..3e0367e1058d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -463,6 +463,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; + [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 6bb4973d0c64..4c8b0e1102b9 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -8,10 +8,12 @@ import _ from 'underscore'; import participantPropTypes from '@components/participantPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import {transactionViolationsPropType} from '@libs/Violations/propTypes'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import stylePropTypes from '@styles/stylePropTypes'; @@ -63,8 +65,13 @@ const propTypes = { /** The transaction from the parent report action */ transactions: PropTypes.objectOf(transactionPropTypes), + /** List of draft comments */ draftComments: PropTypes.objectOf(PropTypes.string), + + /** The list of transaction violations */ + transactionViolations: transactionViolationsPropType, + ...withCurrentReportIDPropTypes, }; @@ -78,6 +85,7 @@ const defaultProps = { personalDetails: {}, transactions: {}, draftComments: {}, + transactionViolations: {}, ...withCurrentReportIDDefaultProps, }; @@ -98,8 +106,10 @@ function LHNOptionsList({ transactions, draftComments, currentReportID, + transactionViolations, }) { const styles = useThemeStyles(); + const {canUseViolations} = usePermissions(); /** * Function which renders a row in the list * @@ -137,10 +147,26 @@ function LHNOptionsList({ onSelectRow={onSelectRow} preferredLocale={preferredLocale} comment={itemComment} + transactionViolations={transactionViolations} + canUseViolations={canUseViolations} /> ); }, - [currentReportID, draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], + [ + currentReportID, + draftComments, + onSelectRow, + optionMode, + personalDetails, + policy, + preferredLocale, + reportActions, + reports, + shouldDisableFocusOptions, + transactions, + transactionViolations, + canUseViolations, + ], ); return ( @@ -189,5 +215,8 @@ export default compose( draftComments: { key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, }), )(LHNOptionsList); diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index e11bfc3cab98..8bdf065a94fd 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -5,8 +5,10 @@ import _ from 'underscore'; import participantPropTypes from '@components/participantPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import {transactionViolationsPropType} from '@libs/Violations/propTypes'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; @@ -42,6 +44,9 @@ const propTypes = { /** The transaction from the parent report action */ transaction: transactionPropTypes, + /** Any violations associated with the transaction */ + transactionViolations: transactionViolationsPropType, + ...basePropTypes, }; @@ -73,6 +78,8 @@ function OptionRowLHNData({ receiptTransactions, parentReportAction, transaction, + transactionViolations, + canUseViolations, ...propsToForward }) { const reportID = propsToForward.reportID; @@ -85,9 +92,19 @@ function OptionRowLHNData({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [fullReport.reportID, receiptTransactions, reportActions]); + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction); + const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction); + const item = SidebarUtils.getOptionData({ + report: fullReport, + reportActions, + personalDetails, + preferredLocale, + policy, + parentReportAction, + hasViolations, + }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; } @@ -96,7 +113,7 @@ function OptionRowLHNData({ // Listen parentReportAction to update title of thread report when parentReportAction changed // Listen to transaction to update title of transaction report when transaction changed // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); + }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction, transactionViolations, canUseViolations]); useEffect(() => { if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index c7d038888c39..7a0a8286f901 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -9,7 +9,7 @@ import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import CONST from '@src/CONST'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import MessagesRow from './MessagesRow'; /** @@ -82,10 +82,10 @@ function OfflineWithFeedback({ const StyleUtils = useStyleUtils(); const {isOffline} = useNetwork(); - const hasErrors = isNotEmptyObject(errors ?? {}); + const hasErrors = !isEmptyObject(errors ?? {}); // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. const errorMessages = omitBy(errors, (e) => e === null); - const hasErrorMessages = isNotEmptyObject(errorMessages); + const hasErrorMessages = !isEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); const isAddError = hasErrors && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 8483b7a481f2..204c9b5e31d4 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -16,6 +16,7 @@ import Text from '@components/Text'; import transactionPropTypes from '@components/transactionPropTypes'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -27,6 +28,7 @@ import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import {transactionViolationsPropType} from '@libs/Violations/propTypes'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import * as IOU from '@userActions/IOU'; @@ -108,6 +110,9 @@ const propTypes = { /** All the transactions, used to update ReportPreview label and status */ transactions: PropTypes.objectOf(transactionPropTypes), + /** All of the transaction violations */ + transactionViolations: transactionViolationsPropType, + ...withLocalizePropTypes, }; @@ -121,6 +126,9 @@ const defaultProps = { accountID: null, }, isWhisper: false, + transactionViolations: { + violations: [], + }, policy: { isHarvestingEnabled: false, }, @@ -131,6 +139,7 @@ function ReportPreview(props) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); + const {canUseViolations} = usePermissions(); const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyDistanceRequests, hasNonReimbursableTransactions} = useMemo( () => ({ @@ -162,7 +171,7 @@ function ReportPreview(props) { const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; const hasReceipts = transactionsWithReceipts.length > 0; const isScanning = hasReceipts && areAllRequestsBeingSmartScanned; - const hasErrors = hasReceipts && hasMissingSmartscanFields; + const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; @@ -365,5 +374,8 @@ export default compose( transactions: { key: ONYXKEYS.COLLECTION.TRANSACTION, }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, }), )(ReportPreview); diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx index 8ef837ed986d..cbd166d79d3a 100644 --- a/src/components/ReportActionItem/TaskPreview.tsx +++ b/src/components/ReportActionItem/TaskPreview.tsx @@ -34,7 +34,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; type PolicyRole = { /** The role of current user */ - role: string; + role: Task.PolicyValue | undefined; }; type TaskPreviewOnyxProps = { @@ -94,7 +94,7 @@ function TaskPreview({ ? taskReport?.stateNum === CONST.REPORT.STATE_NUM.APPROVED && taskReport.statusNum === CONST.REPORT.STATUS_NUM.APPROVED : action?.childStateNum === CONST.REPORT.STATE_NUM.APPROVED && action?.childStatusNum === CONST.REPORT.STATUS_NUM.APPROVED; const taskTitle = Str.htmlEncode(TaskUtils.getTaskTitle(taskReportID, action?.childReportName ?? '')); - const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport ?? {}) ?? action?.childManagerAccountID ?? ''; + const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(taskReport) ?? action?.childManagerAccountID ?? ''; const assigneeLogin = personalDetails[taskAssigneeAccountID]?.login ?? ''; const assigneeDisplayName = personalDetails[taskAssigneeAccountID]?.displayName ?? ''; const taskAssignee = assigneeDisplayName || LocalePhoneNumber.formatPhoneNumber(assigneeLogin); @@ -124,12 +124,12 @@ function TaskPreview({ style={[styles.mr2]} containerStyle={[styles.taskCheckbox]} isChecked={isTaskCompleted} - disabled={!Task.canModifyTask(taskReport ?? {}, currentUserPersonalDetails.accountID, rootParentReportpolicy?.role ?? '')} + disabled={!Task.canModifyTask(taskReport, currentUserPersonalDetails.accountID, rootParentReportpolicy?.role)} onPress={Session.checkIfActionIsAllowed(() => { if (isTaskCompleted) { - Task.reopenTask(taskReport ?? {}); + Task.reopenTask(taskReport); } else { - Task.completeTask(taskReport ?? {}); + Task.completeTask(taskReport); } })} accessibilityLabel={translate('task.task')} @@ -154,7 +154,7 @@ export default withCurrentUserPersonalDetails( }, rootParentReportpolicy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? '0'}`, - selector: (policy: Policy | null) => ({role: policy?.role ?? ''}), + selector: (policy: Policy | null) => ({role: policy?.role}), }, })(TaskPreview), ); diff --git a/src/components/SectionList/index.android.tsx b/src/components/SectionList/index.android.tsx index c119e8e9bcf3..1aa9b501146c 100644 --- a/src/components/SectionList/index.android.tsx +++ b/src/components/SectionList/index.android.tsx @@ -1,22 +1,19 @@ import React, {forwardRef} from 'react'; -import type {ForwardedRef} from 'react'; import {SectionList as RNSectionList} from 'react-native'; -import type {SectionListProps} from 'react-native'; +import type ForwardedSectionList from './types'; // eslint-disable-next-line react/function-component-definition -function SectionListWithRef(props: SectionListProps, ref: ForwardedRef>) { - return ( - - ); -} +const SectionListWithRef: ForwardedSectionList = (props, ref) => ( + +); SectionListWithRef.displayName = 'SectionListWithRef'; diff --git a/src/components/SectionList/index.tsx b/src/components/SectionList/index.tsx index 1129b2bdbb8f..4af7ad33705c 100644 --- a/src/components/SectionList/index.tsx +++ b/src/components/SectionList/index.tsx @@ -1,17 +1,16 @@ import React, {forwardRef} from 'react'; -import type {ForwardedRef} from 'react'; import {SectionList as RNSectionList} from 'react-native'; -import type {SectionListProps} from 'react-native'; +import type ForwardedSectionList from './types'; // eslint-disable-next-line react/function-component-definition -function SectionList(props: SectionListProps, ref: ForwardedRef>) { - return ( - - ); -} +const SectionList: ForwardedSectionList = (props, ref) => ( + +); + +SectionList.displayName = 'SectionList'; export default forwardRef(SectionList); diff --git a/src/components/SectionList/types.ts b/src/components/SectionList/types.ts new file mode 100644 index 000000000000..4648172aabfd --- /dev/null +++ b/src/components/SectionList/types.ts @@ -0,0 +1,9 @@ +import type {ForwardedRef} from 'react'; +import type {SectionList, SectionListProps} from 'react-native'; + +type ForwardedSectionList = { + (props: SectionListProps, ref: ForwardedRef): React.ReactNode; + displayName: string; +}; + +export default ForwardedSectionList; diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.js similarity index 73% rename from src/components/SelectionList/BaseListItem.tsx rename to src/components/SelectionList/BaseListItem.js index 59a1c4dd08ce..6a067ea0fe3d 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.js @@ -1,3 +1,4 @@ +import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; @@ -11,10 +12,10 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import RadioListItem from './RadioListItem'; -import type {BaseListItemProps, RadioItem, User} from './types'; +import {baseListItemPropTypes} from './selectionListPropTypes'; import UserListItem from './UserListItem'; -function BaseListItem({ +function BaseListItem({ item, isFocused = false, isDisabled = false, @@ -25,12 +26,13 @@ function BaseListItem({ onDismissError = () => {}, rightHandSideComponent, keyForList, -}: BaseListItemProps) { +}) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const isRadioItem = item.rightElement === undefined; + const isUserItem = lodashGet(item, 'icons.length', 0) > 0; + const ListItem = isUserItem ? UserListItem : RadioListItem; const rightHandSideComponentRender = () => { if (canSelectMultiple || !rightHandSideComponent) { @@ -68,7 +70,7 @@ function BaseListItem({ styles.justifyContentBetween, styles.sidebarLinkInner, styles.userSelectNone, - isRadioItem ? styles.optionRow : styles.peopleRow, + isUserItem ? styles.peopleRow : styles.optionRow, isFocused && styles.sidebarLinkActive, ]} > @@ -98,32 +100,20 @@ function BaseListItem({ )} - - {isRadioItem ? ( - onSelectRow(item)} - showTooltip={showTooltip} - /> - ) : ( - onSelectRow(item)} - showTooltip={showTooltip} - /> - )} + {!canSelectMultiple && item.isSelected && !rightHandSideComponent && ( ({ )} {rightHandSideComponentRender()} - {!!item.invitedSecondaryLogin && ( + {Boolean(item.invitedSecondaryLogin) && ( {translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})} @@ -150,5 +140,6 @@ function BaseListItem({ } BaseListItem.displayName = 'BaseListItem'; +BaseListItem.propTypes = baseListItemPropTypes; export default BaseListItem; diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.js similarity index 76% rename from src/components/SelectionList/BaseSelectionList.tsx rename to src/components/SelectionList/BaseSelectionList.js index cc55b8e4fc17..960618808fd9 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.js @@ -1,8 +1,8 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {ForwardedRef} from 'react'; +import lodashGet from 'lodash/get'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native'; +import _ from 'underscore'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; @@ -13,60 +13,69 @@ import SafeAreaConsumer from '@components/SafeAreaConsumer'; import SectionList from '@components/SectionList'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import withKeyboardState, {keyboardStatePropTypes} from '@components/withKeyboardState'; import useActiveElementRole from '@hooks/useActiveElementRole'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, RadioItem, Section, SectionListDataType, User} from './types'; - -function BaseSelectionList( - { - sections, - canSelectMultiple = false, - onSelectRow, - onSelectAll, - onDismissError, - textInputLabel = '', - textInputPlaceholder = '', - textInputValue = '', - textInputHint, - textInputMaxLength, - inputMode = CONST.INPUT_MODE.TEXT, - onChangeText, - initiallyFocusedOptionKey = '', - onScroll, - onScrollBeginDrag, - headerMessage = '', - confirmButtonText = '', - onConfirm = () => {}, - headerContent, - footerContent, - showScrollIndicator = false, - showLoadingPlaceholder = false, - showConfirmButton = false, - shouldPreventDefaultFocusOnSelectRow = false, - containerStyle, - isKeyboardShown = false, - disableKeyboardShortcuts = false, - children, - shouldStopPropagation = false, - shouldShowTooltips = true, - shouldUseDynamicMaxToRenderPerBatch = false, - rightHandSideComponent, - }: BaseSelectionListProps, - inputRef: ForwardedRef, -) { +import {propTypes as selectionListPropTypes} from './selectionListPropTypes'; + +const propTypes = { + ...keyboardStatePropTypes, + ...selectionListPropTypes, +}; + +function BaseSelectionList({ + sections, + canSelectMultiple = false, + onSelectRow, + onSelectAll, + onDismissError, + textInputLabel = '', + textInputPlaceholder = '', + textInputValue = '', + textInputHint = '', + textInputMaxLength, + inputMode = CONST.INPUT_MODE.TEXT, + onChangeText, + initiallyFocusedOptionKey = '', + onScroll, + onScrollBeginDrag, + headerMessage = '', + confirmButtonText = '', + onConfirm, + headerContent, + footerContent, + showScrollIndicator = false, + showLoadingPlaceholder = false, + showConfirmButton = false, + shouldPreventDefaultFocusOnSelectRow = false, + isKeyboardShown = false, + containerStyle = [], + disableInitialFocusOptionStyle = false, + inputRef = null, + disableKeyboardShortcuts = false, + children, + shouldStopPropagation = false, + shouldShowTooltips = true, + shouldUseDynamicMaxToRenderPerBatch = false, + rightHandSideComponent, +}) { + const theme = useTheme(); const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const listRef = useRef>>(null); - const textInputRef = useRef(null); - const focusTimeoutRef = useRef(null); - const shouldShowTextInput = !!textInputLabel; - const shouldShowSelectAll = !!onSelectAll; + const listRef = useRef(null); + const textInputRef = useRef(null); + const focusTimeoutRef = useRef(null); + const shouldShowTextInput = Boolean(textInputLabel); + const shouldShowSelectAll = Boolean(onSelectAll); const activeElementRole = useActiveElementRole(); const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); @@ -78,24 +87,26 @@ function BaseSelectionList( * - `disabledOptionsIndexes`: Contains the indexes of all the disabled items in the list, to be used by the ArrowKeyFocusManager * - `itemLayouts`: Contains the layout information for each item, header and footer in the list, * so we can calculate the position of any given item when scrolling programmatically + * + * @return {{itemLayouts: [{offset: number, length: number}], disabledOptionsIndexes: *[], allOptions: *[]}} */ - const flattenedSections = useMemo>(() => { - const allOptions: TItem[] = []; + const flattenedSections = useMemo(() => { + const allOptions = []; - const disabledOptionsIndexes: number[] = []; + const disabledOptionsIndexes = []; let disabledIndex = 0; let offset = 0; const itemLayouts = [{length: 0, offset}]; - const selectedOptions: TItem[] = []; + const selectedOptions = []; - sections.forEach((section, sectionIndex) => { + _.each(sections, (section, sectionIndex) => { const sectionHeaderHeight = variables.optionsListSectionHeaderHeight; itemLayouts.push({length: sectionHeaderHeight, offset}); offset += sectionHeaderHeight; - section.data.forEach((item, optionIndex) => { + _.each(section.data, (item, optionIndex) => { // Add item to the general flattened array allOptions.push({ ...item, @@ -104,7 +115,7 @@ function BaseSelectionList( }); // If disabled, add to the disabled indexes array - if (!!section.isDisabled || item.isDisabled) { + if (section.isDisabled || item.isDisabled) { disabledOptionsIndexes.push(disabledIndex); } disabledIndex += 1; @@ -144,19 +155,19 @@ function BaseSelectionList( }, [canSelectMultiple, sections]); // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member - const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey)); + const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey)); // Disable `Enter` shortcut if the active element is a button or checkbox - const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles); + const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole); /** * Scrolls to the desired item index in the section list * - * @param index - the index of the item to scroll to - * @param animated - whether to animate the scroll + * @param {Number} index - the index of the item to scroll to + * @param {Boolean} animated - whether to animate the scroll */ const scrollToIndex = useCallback( - (index: number, animated = true) => { + (index, animated = true) => { const item = flattenedSections.allOptions[index]; if (!listRef.current || !item) { @@ -171,7 +182,7 @@ function BaseSelectionList( // Otherwise, it will cause an index-out-of-bounds error and crash the app. let adjustedSectionIndex = sectionIndex; for (let i = 0; i < sectionIndex; i++) { - if (sections[i].data) { + if (_.isEmpty(lodashGet(sections, `[${i}].data`))) { adjustedSectionIndex--; } } @@ -186,10 +197,10 @@ function BaseSelectionList( /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. * - * @param item - the list item - * @param shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) + * @param {Object} item - the list item + * @param {Boolean} shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) */ - const selectRow = (item: TItem, shouldUnfocusRow = false) => { + const selectRow = (item, shouldUnfocusRow = false) => { // In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item if (canSelectMultiple) { if (sections.length > 1) { @@ -222,15 +233,15 @@ function BaseSelectionList( }; const selectAllRow = () => { - onSelectAll?.(); - + onSelectAll(); if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) { textInputRef.current.focus(); } }; - const selectFocusedOption = () => { - const focusedOption = flattenedSections.allOptions[focusedIndex]; + const selectFocusedOption = (e) => { + const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']); + const focusedOption = focusedItemKey ? _.find(flattenedSections.allOptions, (option) => option.keyForList === focusedItemKey) : flattenedSections.allOptions[focusedIndex]; if (!focusedOption || focusedOption.isDisabled) { return; @@ -243,8 +254,8 @@ function BaseSelectionList( * This function is used to compute the layout of any given item in our list. * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. * - * @param data - This is the same as the data we pass into the component - * @param flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: + * @param {Array} data - This is the same as the data we pass into the component + * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: * * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. * 2. Each section includes a header, even if we don't provide/render one. @@ -252,8 +263,10 @@ function BaseSelectionList( * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] + * + * @returns {Object} */ - const getItemLayout = (data: Array> | null, flatDataArrayIndex: number) => { + const getItemLayout = (data, flatDataArrayIndex) => { const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex]; if (!targetItem) { @@ -271,8 +284,8 @@ function BaseSelectionList( }; }; - const renderSectionHeader = ({section}: {section: SectionListDataType}) => { - if (!section.title || !section.data) { + const renderSectionHeader = ({section}) => { + if (!section.title || _.isEmpty(section.data)) { return null; } @@ -287,10 +300,9 @@ function BaseSelectionList( ); }; - const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { - const indexOffset = section.indexOffset ? section.indexOffset : 0; - const normalizedIndex = index + indexOffset; - const isDisabled = !!section.isDisabled || item.isDisabled; + const renderItem = ({item, index, section}) => { + const normalizedIndex = index + lodashGet(section, 'indexOffset', 0); + const isDisabled = section.isDisabled || item.isDisabled; const isItemFocused = !isDisabled && focusedIndex === normalizedIndex; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const showTooltip = shouldShowTooltips && normalizedIndex < 10; @@ -300,9 +312,11 @@ function BaseSelectionList( item={item} isFocused={isItemFocused} isDisabled={isDisabled} + isHide={!maxToRenderPerBatch} showTooltip={showTooltip} canSelectMultiple={canSelectMultiple} onSelectRow={() => selectRow(item, true)} + disableIsFocusStyle={disableInitialFocusOptionStyle} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} @@ -312,10 +326,11 @@ function BaseSelectionList( }; const scrollToFocusedIndexOnFirstRender = useCallback( - (nativeEvent: LayoutChangeEvent) => { + ({nativeEvent}) => { if (shouldUseDynamicMaxToRenderPerBatch) { - const listHeight = nativeEvent.nativeEvent.layout.height; - const itemHeight = nativeEvent.nativeEvent.layout.y; + const listHeight = lodashGet(nativeEvent, 'layout.height', 0); + const itemHeight = lodashGet(nativeEvent, 'layout.y', 0); + setMaxToRenderPerBatch((Math.ceil(listHeight / itemHeight) || 0) + CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); } @@ -329,7 +344,7 @@ function BaseSelectionList( ); const updateAndScrollToFocusedIndex = useCallback( - (newFocusedIndex: number) => { + (newFocusedIndex) => { setFocusedIndex(newFocusedIndex); scrollToIndex(newFocusedIndex, true); }, @@ -340,12 +355,7 @@ function BaseSelectionList( useFocusEffect( useCallback(() => { if (shouldShowTextInput) { - focusTimeoutRef.current = setTimeout(() => { - if (!textInputRef.current) { - return; - } - textInputRef.current.focus(); - }, CONST.ANIMATED_TRANSITION); + focusTimeoutRef.current = setTimeout(() => textInputRef.current.focus(), CONST.ANIMATED_TRANSITION); } return () => { if (!focusTimeoutRef.current) { @@ -372,7 +382,7 @@ function BaseSelectionList( /** Selects row when pressing Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, - shouldBubble: !flattenedSections.allOptions[focusedIndex], + shouldBubble: () => !flattenedSections.allOptions[focusedIndex], shouldStopPropagation, isActive: !disableKeyboardShortcuts && !disableEnterShortcut && isFocused, }); @@ -380,8 +390,8 @@ function BaseSelectionList( /** Calls confirm action when pressing CTRL (CMD) + Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, { captureOnInputs: true, - shouldBubble: !flattenedSections.allOptions[focusedIndex], - isActive: !disableKeyboardShortcuts && !!onConfirm && isFocused, + shouldBubble: () => !flattenedSections.allOptions[focusedIndex], + isActive: !disableKeyboardShortcuts && Boolean(onConfirm) && isFocused, }); return ( @@ -391,22 +401,19 @@ function BaseSelectionList( maxIndex={flattenedSections.allOptions.length - 1} onFocusedIndexChanged={updateAndScrollToFocusedIndex} > + {/* */} {({safeAreaPaddingBottomStyle}) => ( - + {shouldShowTextInput && ( { - textInputRef.current = element as RNTextInput; - - if (!inputRef) { - return; - } - - if (typeof inputRef === 'function') { - inputRef(element as RNTextInput); + ref={(el) => { + if (inputRef) { + // eslint-disable-next-line no-param-reassign + inputRef.current = el; } + textInputRef.current = el; }} label={textInputLabel} accessibilityLabel={textInputLabel} @@ -420,16 +427,16 @@ function BaseSelectionList( selectTextOnFocus spellCheck={false} onSubmitEditing={selectFocusedOption} - blurOnSubmit={!!flattenedSections.allOptions.length} + blurOnSubmit={Boolean(flattenedSections.allOptions.length)} /> )} - {!!headerMessage && ( + {Boolean(headerMessage) && ( {headerMessage} )} - {!!headerContent && headerContent} + {Boolean(headerContent) && headerContent} {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( ) : ( @@ -465,9 +472,9 @@ function BaseSelectionList( getItemLayout={getItemLayout} onScroll={onScroll} onScrollBeginDrag={onScrollBeginDrag} - keyExtractor={(item: TItem) => item.keyForList} + keyExtractor={(item) => item.keyForList} extraData={focusedIndex} - indicatorStyle="white" + indicatorStyle={theme.white} keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={showScrollIndicator} initialNumToRender={12} @@ -493,7 +500,7 @@ function BaseSelectionList( /> )} - {!!footerContent && {footerContent}} + {Boolean(footerContent) && {footerContent}} )} @@ -502,5 +509,6 @@ function BaseSelectionList( } BaseSelectionList.displayName = 'BaseSelectionList'; +BaseSelectionList.propTypes = propTypes; -export default forwardRef(BaseSelectionList); +export default withKeyboardState(BaseSelectionList); diff --git a/src/components/SelectionList/RadioListItem.tsx b/src/components/SelectionList/RadioListItem.js similarity index 87% rename from src/components/SelectionList/RadioListItem.tsx rename to src/components/SelectionList/RadioListItem.js index 769eaa80df4b..2de0c96932ea 100644 --- a/src/components/SelectionList/RadioListItem.tsx +++ b/src/components/SelectionList/RadioListItem.js @@ -3,11 +3,10 @@ import {View} from 'react-native'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {RadioListItemProps} from './types'; +import {radioListItemPropTypes} from './selectionListPropTypes'; -function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}: RadioListItemProps) { +function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}) { const styles = useThemeStyles(); - return ( - {!!item.alternateText && ( + {Boolean(item.alternateText) && ( - {!!item.icons && ( + {Boolean(item.icons) && ( )} @@ -23,19 +26,19 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style text={item.text} > {item.text} - {!!item.alternateText && ( + {Boolean(item.alternateText) && ( {item.alternateText} @@ -43,11 +46,12 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style )} - {!!item.rightElement && item.rightElement} + {Boolean(item.rightElement) && item.rightElement} ); } UserListItem.displayName = 'UserListItem'; +UserListItem.propTypes = userListItemPropTypes; export default UserListItem; diff --git a/src/components/SelectionList/index.android.js b/src/components/SelectionList/index.android.js new file mode 100644 index 000000000000..53d5b6bbce06 --- /dev/null +++ b/src/components/SelectionList/index.android.js @@ -0,0 +1,17 @@ +import React, {forwardRef} from 'react'; +import {Keyboard} from 'react-native'; +import BaseSelectionList from './BaseSelectionList'; + +const SelectionList = forwardRef((props, ref) => ( + Keyboard.dismiss()} + /> +)); + +SelectionList.displayName = 'SelectionList'; + +export default SelectionList; diff --git a/src/components/SelectionList/index.android.tsx b/src/components/SelectionList/index.android.tsx deleted file mode 100644 index 8487c6e2cc67..000000000000 --- a/src/components/SelectionList/index.android.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React, {forwardRef} from 'react'; -import type {ForwardedRef} from 'react'; -import {Keyboard} from 'react-native'; -import type {TextInput} from 'react-native'; -import BaseSelectionList from './BaseSelectionList'; -import type {BaseSelectionListProps, RadioItem, User} from './types'; - -function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { - return ( - Keyboard.dismiss()} - /> - ); -} - -SelectionList.displayName = 'SelectionList'; - -export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/index.ios.js b/src/components/SelectionList/index.ios.js new file mode 100644 index 000000000000..7f2a282aeb89 --- /dev/null +++ b/src/components/SelectionList/index.ios.js @@ -0,0 +1,16 @@ +import React, {forwardRef} from 'react'; +import {Keyboard} from 'react-native'; +import BaseSelectionList from './BaseSelectionList'; + +const SelectionList = forwardRef((props, ref) => ( + Keyboard.dismiss()} + /> +)); + +SelectionList.displayName = 'SelectionList'; + +export default SelectionList; diff --git a/src/components/SelectionList/index.ios.tsx b/src/components/SelectionList/index.ios.tsx deleted file mode 100644 index 9c32d38314e2..000000000000 --- a/src/components/SelectionList/index.ios.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, {forwardRef} from 'react'; -import type {ForwardedRef} from 'react'; -import {Keyboard} from 'react-native'; -import type {TextInput} from 'react-native'; -import BaseSelectionList from './BaseSelectionList'; -import type {BaseSelectionListProps, RadioItem, User} from './types'; - -function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { - return ( - - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} - ref={ref} - onScrollBeginDrag={() => Keyboard.dismiss()} - /> - ); -} - -SelectionList.displayName = 'SelectionList'; - -export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/index.tsx b/src/components/SelectionList/index.js similarity index 82% rename from src/components/SelectionList/index.tsx rename to src/components/SelectionList/index.js index 93754926cacb..24ea60d29be5 100644 --- a/src/components/SelectionList/index.tsx +++ b/src/components/SelectionList/index.js @@ -1,12 +1,9 @@ import React, {forwardRef, useEffect, useState} from 'react'; -import type {ForwardedRef} from 'react'; import {Keyboard} from 'react-native'; -import type {TextInput} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import BaseSelectionList from './BaseSelectionList'; -import type {BaseSelectionListProps, RadioItem, User} from './types'; -function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { +const SelectionList = forwardRef((props, ref) => { const [isScreenTouched, setIsScreenTouched] = useState(false); const touchStart = () => setIsScreenTouched(true); @@ -42,8 +39,8 @@ function SelectionList(props: BaseSelectionListP }} /> ); -} +}); SelectionList.displayName = 'SelectionList'; -export default forwardRef(SelectionList); +export default SelectionList; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts deleted file mode 100644 index 5c28a139903d..000000000000 --- a/src/components/SelectionList/types.ts +++ /dev/null @@ -1,277 +0,0 @@ -import type {ReactElement, ReactNode} from 'react'; -import type {GestureResponderEvent, InputModeOptions, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native'; -import type {SubAvatar} from '@components/SubscriptAvatar'; -import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type ChildrenProps from '@src/types/utils/ChildrenProps'; - -type CommonListItemProps = { - /** Whether this item is focused (for arrow key controls) */ - isFocused?: boolean; - - /** Style to be applied to Text */ - textStyles?: StyleProp; - - /** Style to be applied on the alternate text */ - alternateTextStyles?: StyleProp; - - /** Whether this item is disabled */ - isDisabled?: boolean; - - /** Whether this item should show Tooltip */ - showTooltip: boolean; - - /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */ - canSelectMultiple?: boolean; - - /** Callback to fire when the item is pressed */ - onSelectRow: (item: TItem) => void; - - /** Callback to fire when an error is dismissed */ - onDismissError?: (item: TItem) => void; - - /** Component to display on the right side */ - rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; -}; - -type User = { - /** Text to display */ - text: string; - - /** Alternate text to display */ - alternateText?: string; - - /** Key used internally by React */ - keyForList: string; - - /** Whether this option is selected */ - isSelected?: boolean; - - /** Whether this option is disabled for selection */ - isDisabled?: boolean; - - /** User accountID */ - accountID?: number; - - /** User login */ - login?: string; - - /** Element to show on the right side of the item */ - rightElement: ReactElement; - - /** Icons for the user (can be multiple if it's a Workspace) */ - icons?: SubAvatar[]; - - /** Errors that this user may contain */ - errors?: Errors; - - /** The type of action that's pending */ - pendingAction?: PendingAction; - - invitedSecondaryLogin?: string; - - /** Represents the index of the section it came from */ - sectionIndex: number; - - /** Represents the index of the option within the section it came from */ - index: number; -}; - -type UserListItemProps = CommonListItemProps & { - /** The section list item */ - item: User; - - /** Additional styles to apply to text */ - style?: StyleProp; -}; - -type RadioItem = { - /** Text to display */ - text: string; - - /** Alternate text to display */ - alternateText?: string; - - /** Key used internally by React */ - keyForList: string; - - /** Whether this option is selected */ - isSelected?: boolean; - - /** Element to show on the right side of the item */ - rightElement?: undefined; - - /** Whether this option is disabled for selection */ - isDisabled?: undefined; - - invitedSecondaryLogin?: undefined; - - /** Errors that this user may contain */ - errors?: undefined; - - /** The type of action that's pending */ - pendingAction?: undefined; - - /** Represents the index of the section it came from */ - sectionIndex: number; - - /** Represents the index of the option within the section it came from */ - index: number; -}; - -type RadioListItemProps = CommonListItemProps & { - /** The section list item */ - item: RadioItem; -}; - -type BaseListItemProps = CommonListItemProps & { - item: TItem; - shouldPreventDefaultFocusOnSelectRow?: boolean; - keyForList?: string; -}; - -type Section = { - /** Title of the section */ - title?: string; - - /** The initial index of this section given the total number of options in each section's data array */ - indexOffset?: number; - - /** Array of options */ - data: TItem[]; - - /** Whether this section items disabled for selection */ - isDisabled?: boolean; -}; - -type BaseSelectionListProps = Partial & { - /** Sections for the section list */ - sections: Array>; - - /** Whether this is a multi-select list */ - canSelectMultiple?: boolean; - - /** Callback to fire when a row is pressed */ - onSelectRow: (item: TItem) => void; - - /** Callback to fire when "Select All" checkbox is pressed. Only use along with `canSelectMultiple` */ - onSelectAll?: () => void; - - /** Callback to fire when an error is dismissed */ - onDismissError?: () => void; - - /** Label for the text input */ - textInputLabel?: string; - - /** Placeholder for the text input */ - textInputPlaceholder?: string; - - /** Hint for the text input */ - textInputHint?: string; - - /** Value for the text input */ - textInputValue?: string; - - /** Max length for the text input */ - textInputMaxLength?: number; - - /** Callback to fire when the text input changes */ - onChangeText?: (text: string) => void; - - /** Input mode for the text input */ - inputMode?: InputModeOptions; - - /** Item `keyForList` to focus initially */ - initiallyFocusedOptionKey?: string; - - /** Callback to fire when the list is scrolled */ - onScroll?: () => void; - - /** Callback to fire when the list is scrolled and the user begins dragging */ - onScrollBeginDrag?: () => void; - - /** Message to display at the top of the list */ - headerMessage?: string; - - /** Text to display on the confirm button */ - confirmButtonText?: string; - - /** Callback to fire when the confirm button is pressed */ - onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined) => void; - - /** Whether to show the vertical scroll indicator */ - showScrollIndicator?: boolean; - - /** Whether to show the loading placeholder */ - showLoadingPlaceholder?: boolean; - - /** Whether to show the default confirm button */ - showConfirmButton?: boolean; - - /** Whether tooltips should be shown */ - shouldShowTooltips?: boolean; - - /** Whether to stop automatic form submission on pressing enter key or not */ - shouldStopPropagation?: boolean; - - /** Whether to prevent default focusing of options and focus the textinput when selecting an option */ - shouldPreventDefaultFocusOnSelectRow?: boolean; - - /** Custom content to display in the header */ - headerContent?: ReactNode; - - /** Custom content to display in the footer */ - footerContent?: ReactNode; - - /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */ - shouldUseDynamicMaxToRenderPerBatch?: boolean; - - /** Whether keyboard shortcuts should be disabled */ - disableKeyboardShortcuts?: boolean; - - /** Whether to disable initial styling for focused option */ - disableInitialFocusOptionStyle?: boolean; - - /** Styles to apply to SelectionList container */ - containerStyle?: ViewStyle; - - /** Whether keyboard is visible on the screen */ - isKeyboardShown?: boolean; - - /** Whether focus event should be delayed */ - shouldDelayFocus?: boolean; - - /** Component to display on the right side of each child */ - rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; -}; - -type ItemLayout = { - length: number; - offset: number; -}; - -type FlattenedSectionsReturn = { - allOptions: TItem[]; - selectedOptions: TItem[]; - disabledOptionsIndexes: number[]; - itemLayouts: ItemLayout[]; - allSelected: boolean; -}; - -type ButtonOrCheckBoxRoles = 'button' | 'checkbox'; - -type SectionListDataType = SectionListData>; - -export type { - BaseSelectionListProps, - CommonListItemProps, - UserListItemProps, - Section, - RadioListItemProps, - BaseListItemProps, - User, - RadioItem, - FlattenedSectionsReturn, - ItemLayout, - ButtonOrCheckBoxRoles, - SectionListDataType, -}; diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 2e2ae6d06e0f..00cf248ad838 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -104,4 +104,3 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV SubscriptAvatar.displayName = 'SubscriptAvatar'; export default memo(SubscriptAvatar); -export type {SubAvatar}; diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 24acdf6c5f0b..d39f03c5aad4 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -32,7 +32,7 @@ function TaskHeaderActionButton({report, session, policy}: TaskHeaderActionButto