diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e08d44c8279d..3c699bdd4ccf 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8069,6 +8069,8 @@ const CONST = { MONEY_REQUEST_REPORT_ACTIONS_LIST_SELECT_ALL: 'MoneyRequestReportActionsList-SelectAll', MONEY_REQUEST_REPORT_TRANSACTION_ITEM: 'MoneyRequestReportTransactionItem', REPORT_ACTION_AVATAR: 'Report-ReportActionAvatar', + PARTICIPANTS_ROW: 'Report-ParticipantsRow', + ROOM_MEMBERS_ROW: 'Report-RoomMembersRow', }, SIDEBAR: { SIGN_IN_BUTTON: 'Sidebar-SignInButton', @@ -8418,6 +8420,9 @@ const CONST = { MORE_DROPDOWN: 'WorkspaceTags-MoreDropdown', BULK_ACTIONS_DROPDOWN: 'WorkspaceTags-BulkActionsDropdown', }, + REPORT_FIELDS: { + LIST_VALUE_ROW: 'WorkspaceReportFields-ListValueRow', + }, TAXES: { ROW: 'WorkspaceTaxes-Row', ADD_BUTTON: 'WorkspaceTaxes-AddButton', diff --git a/src/components/SelectionListWithModal/CustomListHeader.tsx b/src/components/SelectionListWithModal/CustomListHeader.tsx deleted file mode 100644 index 7f243ab5dac6..000000000000 --- a/src/components/SelectionListWithModal/CustomListHeader.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import Text from '@components/Text'; - -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; - -import type {StyleProp, ViewStyle} from 'react-native'; - -import React from 'react'; -import {View} from 'react-native'; - -type CustomListHeaderProps = { - canSelectMultiple: boolean | undefined; - leftHeaderText?: string | undefined; - rightHeaderText?: string | undefined; - rightHeaderMinimumWidth?: number; - shouldDivideEqualWidth?: boolean; - shouldShowRightCaret?: boolean; - /** Adjusts for fixed width avatar component in the first column */ - shouldAdjustWidthForAvatar?: boolean; - containerStyles?: StyleProp; -}; - -function CustomListHeader({ - canSelectMultiple, - leftHeaderText = '', - rightHeaderText = '', - rightHeaderMinimumWidth = 60, - shouldDivideEqualWidth = false, - shouldShowRightCaret = false, - shouldAdjustWidthForAvatar = false, - containerStyles, -}: CustomListHeaderProps) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - - const header = ( - - {/* mr13 (margin: 52px) accounts for avatar width + spacing */} - - {leftHeaderText} - - - {rightHeaderText} - - - ); - - if (canSelectMultiple) { - return header; - } - return {header}; -} - -export default CustomListHeader; diff --git a/src/components/SelectionListWithModal/ListItemRightCaretWithLabel.tsx b/src/components/SelectionListWithModal/ListItemRightCaretWithLabel.tsx deleted file mode 100644 index ce611e2d704c..000000000000 --- a/src/components/SelectionListWithModal/ListItemRightCaretWithLabel.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import Icon from '@components/Icon'; -import Text from '@components/Text'; - -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; - -import React from 'react'; -import {View} from 'react-native'; - -type ListItemRightCaretWithLabelProps = { - labelText?: string; - shouldShowCaret?: boolean; -}; - -function ListItemRightCaretWithLabel({labelText, shouldShowCaret = false}: ListItemRightCaretWithLabelProps) { - const styles = useThemeStyles(); - const theme = useTheme(); - const StyleUtils = useStyleUtils(); - const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); - - return ( - - {!!labelText && {labelText}} - {shouldShowCaret && ( - - - - )} - - ); -} - -export default ListItemRightCaretWithLabel; diff --git a/src/components/SelectionListWithModal/index.tsx b/src/components/SelectionListWithModal/index.tsx deleted file mode 100644 index c69b5e84d81d..000000000000 --- a/src/components/SelectionListWithModal/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import MenuItem from '@components/MenuItem'; -import Modal from '@components/Modal'; -import SelectionList from '@components/SelectionList'; -import type {ListItem, SelectionListHandle, SelectionListProps} from '@components/SelectionList/types'; - -import useHandleSelectionMode from '@hooks/useHandleSelectionMode'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useThemeStyles from '@hooks/useThemeStyles'; - -import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; - -import CONST from '@src/CONST'; - -import type {ForwardedRef} from 'react'; - -import {useIsFocused} from '@react-navigation/native'; -import React, {useMemo, useState} from 'react'; - -type SelectionListWithModalProps = SelectionListProps & { - turnOnSelectionModeOnLongPress?: boolean; - onTurnOnSelectionMode?: (item: TItem | null) => void; - ref?: ForwardedRef>; -}; - -function SelectionListWithModal({ - turnOnSelectionModeOnLongPress, - onTurnOnSelectionMode, - onLongPressRow, - data, - isSelected, - selectedItems: selectedItemsProp, - style: styleProp, - ref, - ...rest -}: SelectionListWithModalProps) { - const [isModalVisible, setIsModalVisible] = useState(false); - const [longPressedItem, setLongPressedItem] = useState(null); - const {translate} = useLocalize(); - const styles = useThemeStyles(); - // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here because there is a race condition that causes shouldUseNarrowLayout to change indefinitely in this component - // See https://github.com/Expensify/App/issues/48675 for more details - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth} = useResponsiveLayout(); - const isFocused = useIsFocused(); - const icons = useMemoizedLazyExpensifyIcons(['CheckSquare']); - - const isMobileSelectionModeEnabled = useMobileSelectionMode(); - - const selectedItems = useMemo( - () => - selectedItemsProp ?? - data.filter((item) => { - if (isSelected) { - return isSelected(item); - } - return !!item.isSelected; - }) ?? - [], - [isSelected, data, selectedItemsProp], - ); - - useHandleSelectionMode(selectedItems); - - const style = { - ...styleProp, - listHeaderWrapperStyle: [styles.baseListHeaderWrapperStyle, styleProp?.listHeaderWrapperStyle], - }; - - const handleLongPressRow = (item: TItem) => { - if (!turnOnSelectionModeOnLongPress || !isSmallScreenWidth || item?.isDisabled || item?.isDisabledCheckbox || !isFocused) { - return; - } - if (isSmallScreenWidth && isMobileSelectionModeEnabled) { - rest?.onSelectionButtonPress?.(item); - return; - } - - setLongPressedItem(item); - setIsModalVisible(true); - - if (onLongPressRow) { - onLongPressRow(item); - } - }; - - const turnOnSelectionMode = () => { - turnOnMobileSelectionMode(); - setIsModalVisible(false); - - if (onTurnOnSelectionMode) { - onTurnOnSelectionMode(longPressedItem); - } - }; - - return ( - <> - - setIsModalVisible(false)} - shouldPreventScrollOnFocus - > - - - - ); -} - -export default SelectionListWithModal; diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index da7039ea4ddf..d9bd35465f19 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -12,7 +12,7 @@ import CONST from '@src/CONST'; import type {FlashListRef} from '@shopify/flash-list'; -import React, {useImperativeHandle, useRef} from 'react'; +import React, {useEffect, useImperativeHandle, useRef} from 'react'; import type {TableContextValue} from './TableContext'; import type {TableData, TableHandle, TableMethods, TableProps} from './types'; @@ -162,6 +162,7 @@ function Table) { const {translate} = useLocalize(); @@ -182,6 +183,10 @@ function Table({isItemInSearch}); const searchedData = searchMiddleware(filteredData); + useEffect(() => { + onSearchStringChange?.(activeSearchString); + }, [activeSearchString, onSearchStringChange]); + const { activeSorting, methods: sortMethods, diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index f4a812ad972c..c2cfe8855407 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -200,6 +200,9 @@ type TableProps void; + + /** Optional callback fired when the active search string changes. */ + onSearchStringChange?: (searchString: string) => void; }>; export type { diff --git a/src/components/Tables/ReportParticipantsTable/ReportParticipantsTableRow.tsx b/src/components/Tables/ReportParticipantsTable/ReportParticipantsTableRow.tsx index d48ad4866052..3d71019e0de7 100644 --- a/src/components/Tables/ReportParticipantsTable/ReportParticipantsTableRow.tsx +++ b/src/components/Tables/ReportParticipantsTable/ReportParticipantsTableRow.tsx @@ -54,6 +54,7 @@ export default function ReportParticipantsTableRow({item, rowIndex, shouldUseNar rowIndex={rowIndex} disabled={item.disabled} accessibilityLabel={accessibilityLabel} + sentryLabel={CONST.SENTRY_LABEL.REPORT.PARTICIPANTS_ROW} offlineWithFeedback={{pendingAction: item.pendingAction}} onPress={item.action} > diff --git a/src/components/Tables/ReportParticipantsTable/index.tsx b/src/components/Tables/ReportParticipantsTable/index.tsx index c0f82f7f0621..d65c4ab6beb0 100644 --- a/src/components/Tables/ReportParticipantsTable/index.tsx +++ b/src/components/Tables/ReportParticipantsTable/index.tsx @@ -13,7 +13,6 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {ListRenderItemInfo} from '@shopify/flash-list'; import React from 'react'; -import {View} from 'react-native'; import ReportParticipantsTableRow from './ReportParticipantsTableRow'; @@ -127,16 +126,9 @@ export default function ReportParticipantsTable({ref, members, isGroupChat, sele keyExtractor={(item) => item.keyForList} onRowSelectionChange={onRowSelectionChange} > - {shouldShowSearchBar && ( - - - - )} + {shouldShowSearchBar && } - + ); } diff --git a/src/components/Tables/RoomMembersTable/RoomMembersTableRow.tsx b/src/components/Tables/RoomMembersTable/RoomMembersTableRow.tsx new file mode 100644 index 000000000000..7f6036e7e713 --- /dev/null +++ b/src/components/Tables/RoomMembersTable/RoomMembersTableRow.tsx @@ -0,0 +1,91 @@ +import Icon from '@components/Icon'; +import ReportActionAvatars from '@components/ReportActionAvatars'; +import Table from '@components/Table'; +import TextWithTooltip from '@components/TextWithTooltip'; + +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import variables from '@styles/variables'; + +import CONST from '@src/CONST'; + +import React from 'react'; +import {View} from 'react-native'; + +import type {RoomMemberRowData} from '.'; + +type RoomMembersTableRowProps = { + /** The room member item for the row */ + item: RoomMemberRowData; + + /** The index of the row relative to all other rows */ + rowIndex: number; +}; + +export default function RoomMembersTableRow({item, rowIndex}: RoomMembersTableRowProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const styleUtils = useStyleUtils(); + const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); + + const accessibilityLabel = `${item.name}, ${item.email}`; + + const getSecondaryAvatarContainerStyle = (hovered: boolean) => [ + styleUtils.getBackgroundAndBorderStyle(theme.sidebar), + hovered ? styleUtils.getBackgroundAndBorderStyle(styles.sidebarLinkHover?.backgroundColor ?? theme.sidebar) : undefined, + ]; + + return ( + + {({hovered}) => ( + <> + + + + + + + + + + + )} + + ); +} diff --git a/src/components/Tables/RoomMembersTable/index.tsx b/src/components/Tables/RoomMembersTable/index.tsx new file mode 100644 index 000000000000..0d920c17fd2d --- /dev/null +++ b/src/components/Tables/RoomMembersTable/index.tsx @@ -0,0 +1,112 @@ +import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn, TableData, TableHandle} from '@components/Table'; +import Table from '@components/Table'; + +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import tokenizedSearch from '@libs/tokenizedSearch'; + +import variables from '@styles/variables'; + +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; + +import type {ListRenderItemInfo} from '@shopify/flash-list'; + +import React from 'react'; + +import RoomMembersTableRow from './RoomMembersTableRow'; + +type RoomMembersTableColumnKey = 'member' | 'actions'; + +type RoomMemberRowData = TableData & { + accountID: number; + login: string; + name: string; + email: string; + disabled?: boolean; + isSelectionDisabled?: boolean; + pendingAction?: OnyxCommon.PendingAction; + errors?: OnyxCommon.Errors; + action: () => void; + dismissError: () => void; +}; + +type RoomMembersTableProps = { + ref?: React.Ref>; + + /** The rows to render in the table */ + members: RoomMemberRowData[]; + + /** Whether multi-selection is enabled */ + selectionEnabled: boolean; + + /** The list of selected row keys */ + selectedKeys: string[]; + + /** Whether to show the find-member search bar */ + shouldShowSearchBar: boolean; + + /** Callback when the set of selected rows changes */ + onRowSelectionChange: (selectedRowKeys: string[]) => void; + + /** Callback when the active search string changes */ + onSearchStringChange?: (searchString: string) => void; +}; + +export default function RoomMembersTable({ref, members, selectionEnabled, selectedKeys, shouldShowSearchBar, onRowSelectionChange, onSearchStringChange}: RoomMembersTableProps) { + const styles = useThemeStyles(); + const {translate, localeCompare} = useLocalize(); + + const columns: Array> = [ + { + key: 'member', + label: translate('common.member'), + sortable: false, + }, + { + key: 'actions', + label: '', + width: variables.tableCaretColumnWidth, + sortable: false, + }, + ]; + + const compareItems: CompareItemsCallback = (item1, item2) => localeCompare(item1.name.toLowerCase(), item2.name.toLowerCase()); + + const isItemInSearch: IsItemInSearchCallback = (item, searchValue) => { + const results = tokenizedSearch([item], searchValue, (option) => [option.name, option.email, option.login]); + return results.length > 0; + }; + + const renderItem = ({item, index}: ListRenderItemInfo) => ( + + ); + + return ( + item.keyForList} + onRowSelectionChange={onRowSelectionChange} + onSearchStringChange={onSearchStringChange} + > + {shouldShowSearchBar && } + + +
+ ); +} + +export type {RoomMembersTableColumnKey, RoomMemberRowData}; diff --git a/src/components/Tables/WorkspaceReportFieldListValuesTable/WorkspaceReportFieldListValuesTableRow.tsx b/src/components/Tables/WorkspaceReportFieldListValuesTable/WorkspaceReportFieldListValuesTableRow.tsx new file mode 100644 index 000000000000..d2dc7938482e --- /dev/null +++ b/src/components/Tables/WorkspaceReportFieldListValuesTable/WorkspaceReportFieldListValuesTableRow.tsx @@ -0,0 +1,77 @@ +import Icon from '@components/Icon'; +import Switch from '@components/Switch'; +import Table from '@components/Table'; +import TextWithTooltip from '@components/TextWithTooltip'; + +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import variables from '@styles/variables'; + +import CONST from '@src/CONST'; + +import React from 'react'; +import {View} from 'react-native'; + +import type {ReportFieldListValueRowData} from '.'; + +type WorkspaceReportFieldListValuesTableRowProps = { + /** The list value item for the row */ + item: ReportFieldListValueRowData; + + /** The index of the row relative to all other rows */ + rowIndex: number; +}; + +export default function WorkspaceReportFieldListValuesTableRow({item, rowIndex}: WorkspaceReportFieldListValuesTableRowProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']); + + const accessibilityLabel = `${item.name}, ${item.enabled ? translate('common.enabled') : translate('common.disabled')}`; + + return ( + + {({hovered}) => ( + <> + + + + + + + + + + + )} + + ); +} diff --git a/src/components/Tables/WorkspaceReportFieldListValuesTable/index.tsx b/src/components/Tables/WorkspaceReportFieldListValuesTable/index.tsx new file mode 100644 index 000000000000..d3781e0818f7 --- /dev/null +++ b/src/components/Tables/WorkspaceReportFieldListValuesTable/index.tsx @@ -0,0 +1,127 @@ +import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn, TableData} from '@components/Table'; +import Table from '@components/Table'; + +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import tokenizedSearch from '@libs/tokenizedSearch'; + +import variables from '@styles/variables'; + +import CONST from '@src/CONST'; + +import type {ListRenderItemInfo} from '@shopify/flash-list'; + +import React from 'react'; + +import WorkspaceReportFieldListValuesTableRow from './WorkspaceReportFieldListValuesTableRow'; + +type ReportFieldListValueColumnKey = 'name' | 'enabled' | 'actions'; + +type ReportFieldListValueRowData = TableData & { + value: string; + name: string; + index: number; + enabled: boolean; + isLocked: boolean; + isSwitchDisabled?: boolean; + action: () => void; + onToggleEnabled: (enabled: boolean) => void; + onDisabledSwitchPress?: () => void; +}; + +type WorkspaceReportFieldListValuesTableProps = { + listValues: ReportFieldListValueRowData[]; + selectionEnabled: boolean; + selectedKeys: string[]; + onRowSelectionChange: (selectedRowKeys: string[]) => void; + EmptyStateComponent: React.ReactElement; +}; + +export default function WorkspaceReportFieldListValuesTable({ + listValues, + selectionEnabled, + selectedKeys, + onRowSelectionChange, + EmptyStateComponent, +}: WorkspaceReportFieldListValuesTableProps) { + const styles = useThemeStyles(); + const {translate, localeCompare} = useLocalize(); + + const columns: Array> = [ + { + key: 'name', + label: translate('common.name'), + sortable: true, + }, + { + key: 'enabled', + label: translate('common.enabled'), + sortable: true, + width: variables.tableSwitchColumnWidth, + styling: { + containerStyles: [styles.justifyContentEnd], + }, + }, + { + key: 'actions', + label: '', + sortable: false, + width: variables.tableCaretColumnWidth, + }, + ]; + + const compareItems: CompareItemsCallback = (item1, item2, activeSorting) => { + const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1; + + if (activeSorting.columnKey === 'enabled') { + const enabled1 = item1.enabled ? 1 : 0; + const enabled2 = item2.enabled ? 1 : 0; + return (enabled1 - enabled2) * orderMultiplier; + } + + return localeCompare(item1.name, item2.name) * orderMultiplier; + }; + + const isItemInSearch: IsItemInSearchCallback = (item, searchValue) => { + const results = tokenizedSearch([item], searchValue, (option) => [option.name, option.value]); + return results.length > 0; + }; + + const renderItem = ({item, index}: ListRenderItemInfo) => ( + + ); + + const isEmpty = listValues.length === 0; + + return ( + item.keyForList} + onRowSelectionChange={onRowSelectionChange} + > + {isEmpty && EmptyStateComponent} + {!isEmpty && ( + <> + {listValues.length >= CONST.STANDARD_LIST_ITEM_LIMIT && } + + + + )} +
+ ); +} + +export type {ReportFieldListValueColumnKey, ReportFieldListValueRowData}; diff --git a/src/components/Tables/WorkspaceViewTagsTable/index.tsx b/src/components/Tables/WorkspaceViewTagsTable/index.tsx new file mode 100644 index 000000000000..9cb0486a675c --- /dev/null +++ b/src/components/Tables/WorkspaceViewTagsTable/index.tsx @@ -0,0 +1,116 @@ +import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn} from '@components/Table'; +import Table from '@components/Table'; +import type {WorkspaceTagTableRowData} from '@components/Tables/WorkspaceTagsTable'; +import WorkspaceTagsTableRow from '@components/Tables/WorkspaceTagsTable/WorkspaceTagsTableRow'; + +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import tokenizedSearch from '@libs/tokenizedSearch'; + +import variables from '@styles/variables'; + +import CONST from '@src/CONST'; + +import type {ListRenderItemInfo} from '@shopify/flash-list'; + +import React from 'react'; + +type WorkspaceViewTagColumnKey = 'name' | 'enabled' | 'actions'; + +type WorkspaceViewTagsTableProps = { + tags: WorkspaceTagTableRowData[]; + hasDependentTags: boolean; + selectionEnabled: boolean; + selectedKeys: string[]; + onRowSelectionChange: (selectedRowKeys: string[]) => void; +}; + +export default function WorkspaceViewTagsTable({tags, hasDependentTags, selectionEnabled, selectedKeys, onRowSelectionChange}: WorkspaceViewTagsTableProps) { + const styles = useThemeStyles(); + const {translate, localeCompare} = useLocalize(); + + const shouldShowEnabledColumn = !hasDependentTags; + + const columns: Array> = [ + { + key: 'name', + label: translate('common.name'), + sortable: true, + }, + ...(shouldShowEnabledColumn + ? [ + { + key: 'enabled' as const, + label: translate('common.enabled'), + sortable: true, + width: variables.tableSwitchColumnWidth, + styling: { + containerStyles: [styles.justifyContentEnd], + }, + }, + ] + : []), + { + key: 'actions', + label: '', + sortable: false, + width: variables.tableCaretColumnWidth, + }, + ]; + + const compareItems: CompareItemsCallback = (item1, item2, activeSorting) => { + const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1; + + if (activeSorting.columnKey === 'enabled') { + const enabled1 = item1.enabled ? 1 : 0; + const enabled2 = item2.enabled ? 1 : 0; + return (enabled1 - enabled2) * orderMultiplier; + } + + return localeCompare(item1.name, item2.name) * orderMultiplier; + }; + + const isItemInSearch: IsItemInSearchCallback = (item, searchValue) => { + const results = tokenizedSearch([item], searchValue, (option) => [option.name, option.value]); + return results.length > 0; + }; + + const renderItem = ({item, index}: ListRenderItemInfo) => ( + + ); + + if (tags.length === 0) { + return null; + } + + return ( + item.keyForList} + onRowSelectionChange={onRowSelectionChange} + > + {tags.length >= CONST.STANDARD_LIST_ITEM_LIMIT && } + + +
+ ); +} + +export type {WorkspaceViewTagColumnKey}; diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 95204ae05c6d..0fffd68e17ef 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -559,26 +559,6 @@ function isSearchStringMatch(searchValue: string, searchText?: string | null, pa return matching; } -function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchValue: string) { - let memberDetails = ''; - if (personalDetail.login) { - memberDetails += ` ${personalDetail.login}`; - } - if (personalDetail.firstName) { - memberDetails += ` ${personalDetail.firstName}`; - } - if (personalDetail.lastName) { - memberDetails += ` ${personalDetail.lastName}`; - } - if (personalDetail.displayName) { - memberDetails += ` ${getDisplayNameOrDefault(personalDetail)}`; - } - if (personalDetail.phoneNumber) { - memberDetails += ` ${personalDetail.phoneNumber}`; - } - return isSearchStringMatch(searchValue.trim(), memberDetails.toLowerCase()); -} - /** * Get IOU report ID of report last action if the action is report action preview */ @@ -3412,7 +3392,6 @@ export { isDisablingOrDeletingLastEnabledTag, isMakingLastRequiredTagListOptional, isPersonalDetailsReady, - isSearchStringMatchUserDetails, optionsOrderBy, orderOptions, orderPersonalDetailsOptions, diff --git a/src/pages/DynamicReportParticipantsPage.tsx b/src/pages/DynamicReportParticipantsPage.tsx index 273662dfda2c..5ca7bd25361c 100755 --- a/src/pages/DynamicReportParticipantsPage.tsx +++ b/src/pages/DynamicReportParticipantsPage.tsx @@ -264,7 +264,7 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr onPress={() => null} isSplitButton={false} options={bulkActionsButtonOptions} - style={[shouldUseNarrowLayout && styles.flexGrow1]} + style={[shouldUseNarrowLayout && styles.flexGrow1, styles.mb5]} isDisabled={!selectedMembers.length} /> ) : ( @@ -274,13 +274,13 @@ function DynamicReportParticipantsPage({report}: DynamicReportParticipantsPagePr text={translate('workspace.invite.member')} icon={icons.Plus} innerStyles={[shouldUseNarrowLayout && styles.alignItemsCenter]} - style={[shouldUseNarrowLayout && styles.flexGrow1]} + style={[shouldUseNarrowLayout && styles.flexGrow1, styles.mb5]} /> )} )} - + getReportAction(report?.parentReportID, report?.parentReportActionID), [report?.parentReportID, report?.parentReportActionID]); const shouldParserToHTML = reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT; const styles = useThemeStyles(); @@ -85,6 +84,7 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { const [didLoadRoomMembers, setDidLoadRoomMembers] = useState(false); const personalDetails = usePersonalDetails(); const isPolicyExpenseChat = useMemo(() => isPolicyExpenseChatUtils(report), [report]); + const tableRef = useRef>(null); const navigateBackToReportDetails = useCallback(() => { Navigation.goBack(backPath); }, [backPath]); @@ -96,11 +96,6 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { [report, personalDetails, reportMetadata], ); - const [searchValue, setSearchValue, searchFilteredAccountIDs] = useSearchResults(participants, (accountID, search) => { - const details = personalDetails?.[accountID]; - return !!details && isSearchStringMatchUserDetails(details, search); - }); - const shouldIncludeMember = useCallback( (participant?: PersonalDetails) => { if (!participant) { @@ -111,7 +106,6 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { const isPendingDelete = pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - // Keep the member only if they're still in the room and not pending removal return isInParticipants && !isPendingDelete; }, [participants, reportMetadata?.pendingChatMembers], @@ -119,7 +113,23 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { const [selectedMembers, setSelectedMembers] = useFilteredSelection(personalDetailsParticipants, shouldIncludeMember); const firstSelectedMember = selectedMembers?.at(0); - const [firstSelectedMemberDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsSelector(firstSelectedMember)}); + const [firstSelectedMemberDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { + selector: personalDetailsSelector(firstSelectedMember), + }); + + const clearTableSelection = useCallback(() => { + setSelectedMembers((prevSelectedMembers) => (prevSelectedMembers.length > 0 ? [] : prevSelectedMembers)); + }, [setSelectedMembers]); + + // The Table stores selection as string keys, while this page tracks accountIDs as numbers. + const onRowSelectionChange = useCallback( + (keys: string[]) => { + setSelectedMembers(keys.map(Number)); + }, + [setSelectedMembers], + ); + + useCleanupSelectedOptions(clearTableSelection); const isFocusedScreen = useIsFocused(); const {isOffline} = useNetwork(); @@ -129,10 +139,8 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const canSelectMultiple = isSmallScreenWidth ? isMobileSelectionModeEnabled : true; + const isLoading = !isPersonalDetailsReady(personalDetails) || !didLoadRoomMembers; - /** - * Get members for the current room - */ const getRoomMembers = useCallback(() => { if (!report) { return; @@ -147,34 +155,32 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - /** - * Open the modal to invite a user - */ + const clearTableSearch = useCallback(() => { + tableRef.current?.updateSearchString(''); + }, []); + const inviteUser = useCallback(() => { if (!report) { return; } - setSearchValue(''); + clearTableSearch(); Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.ROOM_INVITE.path)); - }, [report, setSearchValue]); + }, [clearTableSearch, report]); - /** - * Remove selected users from the room - * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details - */ const removeUsers = useCallback(() => { if (report) { removeFromRoom(report, selectedMembers); } - setSearchValue(''); - - setSelectedMembers([]); + clearTableSearch(); + clearTableSelection(); clearUserSearchPhrase(); - }, [report, selectedMembers, setSearchValue, setSelectedMembers]); + }, [clearTableSearch, clearTableSelection, report, selectedMembers]); const showRemoveMembersModal = useCallback(async () => { const {action} = await showConfirmModal({ - title: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), + title: translate('workspace.people.removeMembersTitle', { + count: selectedMembers.length, + }), prompt: translate('roomMembersPage.removeMembersPrompt', { count: selectedMembers.length, memberName: formatPhoneNumber(firstSelectedMemberDetails?.displayName ?? ''), @@ -189,110 +195,58 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { removeUsers(); }, [showConfirmModal, translate, selectedMembers.length, formatPhoneNumber, firstSelectedMemberDetails?.displayName, removeUsers]); - /** - * Add user from the selectedMembers list - */ - const addUser = useCallback( - (accountID: number) => { - setSelectedMembers((prevSelected) => [...prevSelected, accountID]); - }, - [setSelectedMembers], - ); - - /** - * Remove user from the selectedEmployees list - */ - const removeUser = useCallback( - (accountID: number) => { - setSelectedMembers((prevSelected) => prevSelected.filter((id) => id !== accountID)); - }, - [setSelectedMembers], - ); - - /** Toggle user from the selectedMembers list */ - const toggleUser = useCallback( - ({accountID, pendingAction}: ListItem) => { - if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !accountID) { - return; - } - - // Add or remove the user if the checkbox is enabled - if (selectedMembers.includes(accountID)) { - removeUser(accountID); - } else { - addUser(accountID); - } - }, - [selectedMembers, addUser, removeUser], - ); - - /** Add or remove all users passed from the selectedMembers list */ - const toggleAllUsers = (memberList: ListItem[]) => { - const enabledAccounts = memberList.filter((member) => !member.isDisabled && !member.isDisabledCheckbox); - const someSelected = enabledAccounts.some((member) => { - if (!member.accountID) { - return false; - } - return selectedMembers.includes(member.accountID); - }); - - if (someSelected) { - setSelectedMembers([]); - } else { - const everyAccountId = enabledAccounts.map((member) => member.accountID).filter((accountID): accountID is number => !!accountID); - setSelectedMembers(everyAccountId); - } - }; - - /** Include the search bar when there are STANDARD_LIST_ITEM_LIMIT or more active members in the selection list */ - const shouldShowTextInput = useMemo(() => { - // Get the active chat members by filtering out the pending members with delete action + const shouldShowSearchBar = useMemo(() => { const activeParticipants = participants.filter((accountID) => { const pendingMember = reportMetadata?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); if (!personalDetails?.[accountID]) { return false; } - // When offline, we want to include the pending members with delete action as they are displayed in the list as well return !pendingMember || isOffline || pendingMember.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; }); return activeParticipants.length >= CONST.STANDARD_LIST_ITEM_LIMIT; }, [participants, reportMetadata?.pendingChatMembers, personalDetails, isOffline]); useEffect(() => { - if (!isFocusedScreen || !shouldShowTextInput) { + if (!isFocusedScreen || !shouldShowSearchBar || isLoading) { return; } - setSearchValue(userSearchPhrase ?? ''); - }, [isFocusedScreen, setSearchValue, shouldShowTextInput, userSearchPhrase]); - useEffect(() => { - updateUserSearchPhrase(searchValue); - }, [searchValue]); + const phrase = userSearchPhrase ?? ''; + const currentSearchString = tableRef.current?.getActiveSearchString?.() ?? ''; + + if (currentSearchString === phrase) { + return; + } + + tableRef.current?.updateSearchString(phrase); + }, [isFocusedScreen, isLoading, shouldShowSearchBar, userSearchPhrase]); useEffect(() => { if (!isFocusedScreen) { return; } - if (shouldShowTextInput) { - setSearchValue(userSearchPhrase ?? ''); - } else { + if (!shouldShowSearchBar) { clearUserSearchPhrase(); - setSearchValue(''); + clearTableSearch(); } - }, [isFocusedScreen, setSearchValue, shouldShowTextInput, userSearchPhrase]); + }, [clearTableSearch, isFocusedScreen, shouldShowSearchBar]); useSearchBackPress({ - onClearSelection: () => setSelectedMembers([]), + onClearSelection: clearTableSelection, onNavigationCallBack: () => { - setSearchValue(''); + clearTableSearch(); navigateBackToReportDetails(); }, }); - const data = useMemo((): ListItem[] => { - let result: ListItem[] = []; + const openRoomMemberDetails = useCallback((accountID: number) => { + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.ROOM_MEMBER_DETAILS.getRoute(accountID))); + }, []); + + const members = useMemo(() => { + const result: RoomMemberRowData[] = []; - for (const accountID of searchFilteredAccountIDs) { + for (const accountID of participants) { const details = personalDetails?.[accountID]; if (!details) { continue; @@ -300,7 +254,7 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { const pendingChatMember = reportMetadata?.pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); const isAdmin = isPolicyAdmin(policy, details.login); const isDisabled = pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || details.isOptimisticPersonalDetail; - const isDisabledCheckbox = + const isSelectionDisabled = (isPolicyExpenseChat && isAdmin) || accountID === session?.accountID || pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || @@ -309,55 +263,49 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { result.push({ keyForList: String(accountID), accountID, - isSelected: selectedMembers.includes(accountID), - isDisabled, - isDisabledCheckbox, - text: formatPhoneNumber(temporaryGetDisplayNameOrDefault({passedPersonalDetails: details, translate})), - alternateText: details?.login ? formatPhoneNumber(details.login) : '', - icons: [ - { - source: details.avatar ?? icons.FallbackAvatar, - name: details.login ?? '', - type: CONST.ICON_TYPE_AVATAR, - id: accountID, - }, - ], + login: details.login ?? '', + name: formatPhoneNumber( + temporaryGetDisplayNameOrDefault({ + passedPersonalDetails: details, + translate, + }), + ), + email: formatPhoneNumber(details.login ?? ''), + disabled: isDisabled, + isSelectionDisabled, pendingAction: pendingChatMember?.pendingAction, errors: pendingChatMember?.errors, + action: () => openRoomMemberDetails(accountID), + dismissError: () => clearAddRoomMemberError(report.reportID, String(accountID)), }); } - result = result.sort((value1, value2) => localeCompare(value1.text ?? '', value2.text ?? '')); - - return result; + return result.sort((value1, value2) => localeCompare(value1.name.toLowerCase(), value2.name.toLowerCase())); }, [ formatPhoneNumber, - localeCompare, isPolicyExpenseChat, - searchFilteredAccountIDs, + localeCompare, + openRoomMemberDetails, + participants, personalDetails, policy, report.ownerAccountID, + report.reportID, reportMetadata?.pendingChatMembers, - selectedMembers, session?.accountID, - icons.FallbackAvatar, translate, ]); - const dismissError = useCallback( - (item: ListItem) => { - clearAddRoomMemberError(report.reportID, String(item.accountID)); - }, - [report.reportID], - ); + const selectedKeys = selectedMembers.map(String); const isPolicyEmployee = useMemo(() => isPolicyEmployeeUtils(report.policyID, policy), [report?.policyID, policy]); const bulkActionsButtonOptions = useMemo(() => { const options: Array> = [ { - text: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), + text: translate('workspace.people.removeMembersTitle', { + count: selectedMembers.length, + }), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE, icon: icons.RemoveMembers, onSelected: showRemoveMembersModal, @@ -373,12 +321,14 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { shouldAlwaysShowDropdownMenu pressOnEnter - customText={translate('workspace.common.selected', {count: selectedMembers.length})} + customText={translate('workspace.common.selected', { + count: selectedMembers.length, + })} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} onPress={() => null} options={bulkActionsButtonOptions} isSplitButton={false} - style={[shouldUseNarrowLayout && styles.flexGrow1]} + style={[shouldUseNarrowLayout && styles.flexGrow1, styles.mb5]} isDisabled={!selectedMembers.length} /> ) : ( @@ -388,48 +338,19 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { text={translate('workspace.invite.member')} icon={icons.Plus} innerStyles={[shouldUseNarrowLayout && styles.alignItemsCenter]} - style={[shouldUseNarrowLayout && styles.flexGrow1]} + style={[shouldUseNarrowLayout && styles.flexGrow1, styles.mb5]} /> )} ); }, [bulkActionsButtonOptions, inviteUser, isSmallScreenWidth, selectedMembers.length, styles, translate, canSelectMultiple, shouldUseNarrowLayout, icons.Plus]); - /** Opens the room member details page */ - const openRoomMemberDetails = useCallback((item: ListItem) => { - if (!item?.accountID) { - return; - } - - Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.ROOM_MEMBER_DETAILS.getRoute(item.accountID))); - }, []); const selectionModeHeader = isMobileSelectionModeEnabled && isSmallScreenWidth; - - const customListHeader = useMemo(() => { - const header = ( - - - {translate('common.member')} - - - ); - - if (canSelectMultiple) { - return header; - } - - return {header}; - }, [styles, translate, canSelectMultiple]); - - const textInputOptions = useMemo( - () => ({ - label: translate('selectionList.findMember'), - value: searchValue, - onChangeText: setSearchValue, - headerMessage: searchValue.trim() && !data.length ? `${translate('roomMembersPage.memberNotFound')} ${translate('roomMembersPage.useInviteButton')}` : '', - }), - [data.length, searchValue, setSearchValue, translate], - ); + const reasonAttributes = { + context: 'DynamicRoomMembersPage', + didLoadRoomMembers, + isPersonalDetailsReady: isPersonalDetailsReady(personalDetails), + }; let subtitleKey: '' | TranslationPaths | undefined; if (!isEmptyObject(report)) { @@ -454,35 +375,34 @@ function DynamicRoomMembersPage({report, policy}: DynamicRoomMembersPageProps) { )} onBackButtonPress={() => { if (isMobileSelectionModeEnabled) { - setSelectedMembers([]); + clearTableSelection(); turnOffMobileSelectionMode(); return; } - setSearchValue(''); + clearTableSearch(); navigateBackToReportDetails(); }} /> {headerButtons} - - item && toggleUser(item)} - onSelectAll={() => toggleAllUsers(data)} - canSelectMultiple={canSelectMultiple} - selectAllAccessibilityLabel={translate('accessibilityHints.selectAllMembers')} - customListHeader={customListHeader} - onDismissError={dismissError} - turnOnSelectionModeOnLongPress + {isLoading ? ( + - + ) : ( + + + + )} ); diff --git a/src/pages/workspace/reports/ReportFieldsListValuesPage.tsx b/src/pages/workspace/reports/ReportFieldsListValuesPage.tsx index 4e57c196f26b..66673426731c 100644 --- a/src/pages/workspace/reports/ReportFieldsListValuesPage.tsx +++ b/src/pages/workspace/reports/ReportFieldsListValuesPage.tsx @@ -6,15 +6,13 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import SearchBar from '@components/SearchBar'; -import TableListItem from '@components/SelectionList/ListItem/TableListItem'; -import type {ListItem} from '@components/SelectionList/types'; -import SelectionListWithModal from '@components/SelectionListWithModal'; -import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; -import Switch from '@components/Switch'; +import type {ReportFieldListValueRowData} from '@components/Tables/WorkspaceReportFieldListValuesTable'; +import WorkspaceReportFieldListValuesTable from '@components/Tables/WorkspaceReportFieldListValuesTable'; import Text from '@components/Text'; +import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useConfirmModal from '@hooks/useConfirmModal'; +import useFilteredSelection from '@hooks/useFilteredSelection'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -22,7 +20,6 @@ import useOnyx from '@hooks/useOnyx'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; -import useSearchResults from '@hooks/useSearchResults'; import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -33,12 +30,10 @@ import { setReportFieldsListValueEnabled, updateReportFieldListValueEnabled as updateReportFieldListValueEnabledReportField, } from '@libs/actions/Policy/ReportField'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {hasAccountingConnections as hasAccountingConnectionsPolicyUtils} from '@libs/PolicyUtils'; import {getReportFieldKey} from '@libs/ReportUtils'; -import StringUtils from '@libs/StringUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -52,20 +47,9 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; -type ValueListItem = ListItem & { - /** The value */ - value: string; - - /** Whether the value is enabled */ - enabled: boolean; - - /** The value order weight in the list */ - orderWeight?: number; -}; - type ReportFieldsListValuesPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; function ReportFieldsListValuesPage({ @@ -75,7 +59,7 @@ function ReportFieldsListValuesPage({ }, }: ReportFieldsListValuesPageProps) { const styles = useThemeStyles(); - const {translate, localeCompare} = useLocalize(); + const {translate} = useLocalize(); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here to use the mobile selection mode on small screens only // See https://github.com/Expensify/App/issues/48724 for more details // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -86,11 +70,8 @@ function ReportFieldsListValuesPage({ const {showConfirmModal} = useConfirmModal(); const {canWrite: canWriteReportFields, withReadOnlyFallback} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.REPORT_FIELDS); - const [selectedValues, setSelectedValues] = useState>({}); const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); - const canSelectMultiple = canWriteReportFields && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true); - const [listValues, disabledListValues] = useMemo(() => { let reportFieldValues: string[]; let reportFieldDisabledValues: boolean[]; @@ -111,7 +92,12 @@ function ReportFieldsListValuesPage({ const updateReportFieldListValueEnabled = useCallback( (value: boolean, valueIndex: number) => { if (reportFieldID) { - updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: [Number(valueIndex)], enabled: value}); + updateReportFieldListValueEnabledReportField({ + policy, + reportFieldID, + valueIndexes: [Number(valueIndex)], + enabled: value, + }); return; } @@ -124,74 +110,65 @@ function ReportFieldsListValuesPage({ [disabledListValues, policy, reportFieldID], ); - useSearchBackPress({ - onClearSelection: () => { - setSelectedValues({}); + const openListValuePage = useCallback( + (valueIndex: number) => { + Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS.getRoute(policyID, valueIndex, reportFieldID)); }, - onNavigationCallBack: () => Navigation.goBack(), - }); + [policyID, reportFieldID], + ); - const data = useMemo( + const listValueRows = useMemo( () => - listValues.map((value, index) => ({ + listValues.map((value, index) => ({ + keyForList: value, value, + name: value, index, - text: value, - keyForList: value, - isSelected: selectedValues[value] && canSelectMultiple, enabled: !disabledListValues.at(index), - rightElement: ( - updateReportFieldListValueEnabled(newValue, index)} - disabled={!canWriteReportFields} - disabledAction={withReadOnlyFallback()} - showLockIcon={!canWriteReportFields} - /> - ), + isLocked: !canWriteReportFields, + isSwitchDisabled: !canWriteReportFields, + action: () => openListValuePage(index), + onToggleEnabled: (enabled: boolean) => updateReportFieldListValueEnabled(enabled, index), + onDisabledSwitchPress: withReadOnlyFallback(), })), - [canSelectMultiple, canWriteReportFields, disabledListValues, withReadOnlyFallback, listValues, selectedValues, translate, updateReportFieldListValueEnabled], + [canWriteReportFields, disabledListValues, listValues, openListValuePage, updateReportFieldListValueEnabled, withReadOnlyFallback], ); - const filterListValue = useCallback((item: ValueListItem, searchInput: string) => { - const itemText = StringUtils.normalize(item.text?.toLowerCase() ?? ''); - const normalizedSearchInput = StringUtils.normalize(searchInput.toLowerCase()); - return itemText.includes(normalizedSearchInput); - }, []); - const sortListValues = useCallback((values: ValueListItem[]) => values.sort((a, b) => localeCompare(a.value, b.value)), [localeCompare]); - const [inputValue, setInputValue, filteredListValues] = useSearchResults(data, filterListValue, sortListValues); - const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Close', 'Plus', 'Trashcan']); + const listValueRowsKeyed = useMemo( + () => + listValueRows.reduce>((acc, row) => { + acc[row.keyForList] = row; + return acc; + }, {}), + [listValueRows], + ); - const filteredListValuesArray = filteredListValues.map((item) => item.value); + const filterListValueRow = useCallback((row?: ReportFieldListValueRowData) => !!row, []); - const shouldShowEmptyState = Object.values(listValues ?? {}).length <= 0; - const selectedValuesArray = Object.keys(selectedValues).filter((key) => selectedValues[key] && listValues.includes(key)); + const [selectedKeys, setSelectedKeys] = useFilteredSelection(listValueRowsKeyed, filterListValueRow); - const toggleValue = (valueItem: ValueListItem) => { - setSelectedValues((prev) => ({ - ...prev, - [valueItem.value]: !prev[valueItem.value], - })); - }; + const clearTableSelection = useCallback(() => { + setSelectedKeys((prevSelectedKeys) => (prevSelectedKeys.length > 0 ? [] : prevSelectedKeys)); + }, [setSelectedKeys]); - const toggleAllValues = () => { - setSelectedValues(selectedValuesArray.length > 0 ? {} : Object.fromEntries(filteredListValuesArray.map((value) => [value, true]))); - }; + useCleanupSelectedOptions(clearTableSelection); - const handleDeleteValues = () => { - const valuesToDelete = selectedValuesArray.reduce((acc, valueName) => { - const index = listValues?.indexOf(valueName) ?? -1; + useSearchBackPress({ + onClearSelection: clearTableSelection, + onNavigationCallBack: () => Navigation.goBack(), + }); - if (index !== -1) { - acc.push(index); - } + const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Close', 'Plus', 'Trashcan']); - return acc; - }, []); + const handleDeleteValues = useCallback(() => { + const valuesToDelete = selectedKeys.map((value) => listValues.indexOf(value)).filter((index) => index !== -1); if (reportFieldID) { - removeReportFieldListValue({policy, reportFieldID, valueIndexes: valuesToDelete}); + removeReportFieldListValue({ + policy, + reportFieldID, + valueIndexes: valuesToDelete, + }); } else { deleteReportFieldsListValue({ valueIndexes: valuesToDelete, @@ -200,46 +177,26 @@ function ReportFieldsListValuesPage({ }); } - setSelectedValues({}); - }; - - const openListValuePage = (valueItem: ValueListItem) => { - if (valueItem.index === undefined) { - return; - } - - Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS.getRoute(policyID, valueItem.index, reportFieldID)); - }; + clearTableSelection(); + }, [clearTableSelection, disabledListValues, listValues, policy, reportFieldID, selectedKeys]); - const getCustomListHeader = () => { - if (filteredListValues.length === 0) { - return null; - } - return ( - - ); - }; + const shouldShowEmptyState = listValues.length === 0; const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); const getHeaderButtons = () => { const options: Array>> = []; - if (canWriteReportFields && (isSmallScreenWidth ? isMobileSelectionModeEnabled : selectedValuesArray.length > 0)) { - if (selectedValuesArray.length > 0 && !hasAccountingConnections) { + if (canWriteReportFields && (isSmallScreenWidth ? isMobileSelectionModeEnabled : selectedKeys.length > 0)) { + if (selectedKeys.length > 0 && !hasAccountingConnections) { options.push({ icon: icons.Trashcan, - text: translate(selectedValuesArray.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'), + text: translate(selectedKeys.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'), value: CONST.POLICY.BULK_ACTION_TYPES.DELETE, onSelected: () => { showConfirmModal({ danger: true, - title: translate(selectedValuesArray.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'), - prompt: translate(selectedValuesArray.length === 1 ? 'workspace.reportFields.deleteValuePrompt' : 'workspace.reportFields.deleteValuesPrompt'), + title: translate(selectedKeys.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'), + prompt: translate(selectedKeys.length === 1 ? 'workspace.reportFields.deleteValuePrompt' : 'workspace.reportFields.deleteValuesPrompt'), confirmText: translate('common.delete'), cancelText: translate('common.cancel'), }).then((result) => { @@ -252,15 +209,15 @@ function ReportFieldsListValuesPage({ }, }); } - const enabledValues = selectedValuesArray.filter((valueName) => { - const index = listValues?.indexOf(valueName) ?? -1; - return !disabledListValues?.at(index); + const enabledValues = selectedKeys.filter((value) => { + const index = listValues.indexOf(value); + return index !== -1 && !disabledListValues?.at(index); }); if (enabledValues.length > 0) { - const valuesToDisable = selectedValuesArray.reduce((acc, valueName) => { - const index = listValues?.indexOf(valueName) ?? -1; - if (!disabledListValues?.at(index) && index !== -1) { + const valuesToDisable = selectedKeys.reduce((acc, value) => { + const index = listValues.indexOf(value); + if (index !== -1 && !disabledListValues?.at(index)) { acc.push(index); } @@ -272,10 +229,15 @@ function ReportFieldsListValuesPage({ text: translate(enabledValues.length === 1 ? 'workspace.reportFields.disableValue' : 'workspace.reportFields.disableValues'), value: CONST.POLICY.BULK_ACTION_TYPES.DISABLE, onSelected: () => { - setSelectedValues({}); + clearTableSelection(); if (reportFieldID) { - updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: valuesToDisable, enabled: false}); + updateReportFieldListValueEnabledReportField({ + policy, + reportFieldID, + valueIndexes: valuesToDisable, + enabled: false, + }); return; } @@ -288,15 +250,15 @@ function ReportFieldsListValuesPage({ }); } - const disabledValues = selectedValuesArray.filter((valueName) => { - const index = listValues?.indexOf(valueName) ?? -1; - return disabledListValues?.at(index); + const disabledValues = selectedKeys.filter((value) => { + const index = listValues.indexOf(value); + return index !== -1 && disabledListValues?.at(index); }); if (disabledValues.length > 0) { - const valuesToEnable = selectedValuesArray.reduce((acc, valueName) => { - const index = listValues?.indexOf(valueName) ?? -1; - if (disabledListValues?.at(index) && index !== -1) { + const valuesToEnable = selectedKeys.reduce((acc, value) => { + const index = listValues.indexOf(value); + if (index !== -1 && disabledListValues?.at(index)) { acc.push(index); } @@ -308,10 +270,15 @@ function ReportFieldsListValuesPage({ text: translate(disabledValues.length === 1 ? 'workspace.reportFields.enableValue' : 'workspace.reportFields.enableValues'), value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, onSelected: () => { - setSelectedValues({}); + clearTableSelection(); if (reportFieldID) { - updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: valuesToEnable, enabled: true}); + updateReportFieldListValueEnabledReportField({ + policy, + reportFieldID, + valueIndexes: valuesToEnable, + enabled: true, + }); return; } @@ -329,11 +296,13 @@ function ReportFieldsListValuesPage({ onPress={() => null} shouldAlwaysShowDropdownMenu buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {count: selectedValuesArray.length})} + customText={translate('workspace.common.selected', { + count: selectedKeys.length, + })} options={options} isSplitButton={false} style={[shouldDisplayButtonsInSeparateLine && styles.flexGrow1, shouldDisplayButtonsInSeparateLine && styles.mb3]} - isDisabled={!selectedValuesArray.length} + isDisabled={!selectedKeys.length} /> ); } @@ -354,21 +323,26 @@ function ReportFieldsListValuesPage({ const selectionModeHeader = isMobileSelectionModeEnabled && isSmallScreenWidth; const headerContent = ( - <> - - {translate('workspace.reportFields.listInputSubtitle')} - - {data.length >= CONST.STANDARD_LIST_ITEM_LIMIT && ( - - )} - + + {translate('workspace.reportFields.listInputSubtitle')} + ); + const emptyStateContent = ( + + {headerContent} + + + ); + + const headerButtons = getHeaderButtons(); + return ( { if (isMobileSelectionModeEnabled) { - setSelectedValues({}); + clearTableSelection(); turnOffMobileSelectionMode(); return; } Navigation.goBack(); }} > - {!shouldDisplayButtonsInSeparateLine && getHeaderButtons()} + {!shouldDisplayButtonsInSeparateLine && headerButtons} - {shouldDisplayButtonsInSeparateLine && {getHeaderButtons()}} - {shouldShowEmptyState && ( - - {headerContent} - - - )} - {!shouldShowEmptyState && ( - 0 ? toggleAllValues : undefined} - onTurnOnSelectionMode={(item) => item && canWriteReportFields && toggleValue(item)} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - customListHeader={getCustomListHeader()} - customListHeaderContent={headerContent} - canSelectMultiple={canSelectMultiple} - selectAllAccessibilityLabel={translate('accessibilityHints.selectAllValues')} - onSelectionButtonPress={toggleValue} - shouldShowListEmptyContent={false} - showScrollIndicator={false} - turnOnSelectionModeOnLongPress={canWriteReportFields} - shouldHeaderBeInsideList - shouldShowRightCaret - /> - )} + {shouldDisplayButtonsInSeparateLine && {headerButtons}} + {!shouldShowEmptyState && headerContent} + ); diff --git a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx index f26d1ada6106..3791ac15329a 100644 --- a/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceViewTagsPage.tsx @@ -6,12 +6,8 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; -import SearchBar from '@components/SearchBar'; -import TableListItem from '@components/SelectionList/ListItem/TableListItem'; -import SelectionListWithModal from '@components/SelectionListWithModal'; -import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; -import ListItemRightCaretWithLabel from '@components/SelectionListWithModal/ListItemRightCaretWithLabel'; -import Switch from '@components/Switch'; +import type {WorkspaceTagTableRowData} from '@components/Tables/WorkspaceTagsTable'; +import WorkspaceViewTagsTable from '@components/Tables/WorkspaceViewTagsTable'; import useConfirmModal from '@hooks/useConfirmModal'; import useDynamicBackPath from '@hooks/useDynamicBackPath'; @@ -24,7 +20,6 @@ import usePolicyData from '@hooks/usePolicyData'; import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchBackPress from '@hooks/useSearchBackPress'; -import useSearchResults from '@hooks/useSearchResults'; import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -38,7 +33,6 @@ import { setPolicyTagsRequired, setWorkspaceTagEnabled, } from '@libs/actions/Policy/Tag'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -50,7 +44,6 @@ import { hasDependentTags as hasDependentTagsPolicyUtils, isMultiLevelTags as isMultiLevelTagsPolicyUtils, } from '@libs/PolicyUtils'; -import StringUtils from '@libs/StringUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -69,8 +62,6 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; -import type {TagListItem} from './types'; - type WorkspaceViewTagsProps = | PlatformStackScreenProps | PlatformStackScreenProps; @@ -85,7 +76,7 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); const styles = useThemeStyles(); const icons = useMemoizedLazyExpensifyIcons(['Close', 'Checkmark', 'Trashcan']); - const {translate, localeCompare} = useLocalize(); + const {translate} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const dropdownButtonRef = useRef(null); const isFocused = useIsFocused(); @@ -108,15 +99,6 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const [selectedTags, setSelectedTags] = useFilteredSelection(currentPolicyTag?.tags, filterFunction); const {isOffline} = useNetwork({onReconnect: fetchTags}); - const canSelectMultiple = useMemo(() => { - if (!canWriteTags) { - return false; - } - if (hasDependentTags) { - return false; - } - return isSmallScreenWidth ? isMobileSelectionModeEnabled : true; - }, [canWriteTags, hasDependentTags, isSmallScreenWidth, isMobileSelectionModeEnabled]); useEffect(() => { if (isFocused) { @@ -147,130 +129,92 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { [canWriteTags, policyData, orderWeight, showReadOnlyModal], ); - const tagList = useMemo(() => { + const navigateToTagSettings = useCallback( + (tag: PolicyTag) => { + if (!canWriteTags) { + return; + } + + const parentTagsFilter = tag?.rules?.parentTagsFilter; + const workspaceTagSettingsSuffix = parentTagsFilter + ? `${DYNAMIC_ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(orderWeight, tag.name)}?parentTagsFilter=${encodeURIComponent(parentTagsFilter)}` + : DYNAMIC_ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(orderWeight, tag.name); + + Navigation.navigate( + isQuickSettingsFlow ? createDynamicRoute(DYNAMIC_ROUTES.SETTINGS_TAG_SETTINGS.getRoute(orderWeight, tag.name)) : createDynamicRoute(workspaceTagSettingsSuffix), + ); + }, + [canWriteTags, isQuickSettingsFlow, orderWeight], + ); + + const tagRows = useMemo(() => { const enabledTagsCount = getCountOfEnabledTagsOfList(currentPolicyTag?.tags); - return Object.values(currentPolicyTag?.tags ?? {}).map((tag) => { - const isDisablingLastEnabledTag = isDisablingOrDeletingLastEnabledTag(currentPolicyTag, [tag], enabledTagsCount); - let rightElement; - if (hasDependentTags && canWriteTags) { - rightElement = ; - } else if (!hasDependentTags) { - rightElement = ( - { - if (isDisablingLastEnabledTag) { - showConfirmModal({ - title: translate('workspace.tags.cannotDeleteOrDisableAllTags.title'), - prompt: translate('workspace.tags.cannotDeleteOrDisableAllTags.description'), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - }); - return; - } - updateWorkspaceTagEnabled(newValue, tag.name); - }} - showLockIcon={!canWriteTags || isDisablingLastEnabledTag} - /> - ); + return Object.values(currentPolicyTag?.tags ?? {}).reduce((acc, tag) => { + const isDisabled = tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + + if (!isOffline && isDisabled) { + return acc; } - return { - value: tag.name, - text: hasDependentTags ? tag.name : getCleanedTagName(tag.name), + const isDisablingLastEnabledTag = isDisablingOrDeletingLastEnabledTag(currentPolicyTag, [tag], enabledTagsCount); + + acc.push({ keyForList: hasDependentTags ? `${tag.name}-${tag.rules?.parentTagsFilter ?? ''}` : tag.name, - isSelected: selectedTags.includes(tag.name) && canSelectMultiple, - pendingAction: tag.pendingAction, - rules: tag.rules, - errors: tag.errors ?? undefined, + value: tag.name, + name: hasDependentTags ? tag.name : getCleanedTagName(tag.name), enabled: tag.enabled, - isDisabled: tag.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - rightElement, - }; - }); - }, [currentPolicyTag, hasDependentTags, canWriteTags, selectedTags, canSelectMultiple, translate, updateWorkspaceTagEnabled, showConfirmModal, withReadOnlyFallback]); - - const filterTag = useCallback((tag: TagListItem, searchInput: string) => { - const tagText = StringUtils.normalize(tag.text?.toLowerCase() ?? ''); - const tagValue = StringUtils.normalize(tag.text?.toLowerCase() ?? ''); - const normalizedSearchInput = StringUtils.normalize(searchInput.toLowerCase() ?? ''); - return tagText.includes(normalizedSearchInput) || tagValue.includes(normalizedSearchInput); - }, []); - const sortTags = useCallback((tags: TagListItem[]) => tags.sort((tagA, tagB) => localeCompare(tagA.value, tagB.value)), [localeCompare]); - const [inputValue, setInputValue, filteredTagList] = useSearchResults(tagList, filterTag, sortTags); + disabled: isDisabled, + errors: tag.errors ?? undefined, + pendingAction: tag.pendingAction, + isLocked: !canWriteTags || isDisablingLastEnabledTag, + showEnabledSwitch: !hasDependentTags, + showRequiredSwitch: false, + action: () => navigateToTagSettings(tag), + onToggleEnabled: (enabled: boolean) => { + if (isDisablingLastEnabledTag) { + showConfirmModal({ + title: translate('workspace.tags.cannotDeleteOrDisableAllTags.title'), + prompt: translate('workspace.tags.cannotDeleteOrDisableAllTags.description'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); + return; + } + updateWorkspaceTagEnabled(enabled, tag.name); + }, + onClose: () => + clearPolicyTagErrors({ + policyID, + tagName: tag.name, + tagListIndex: orderWeight, + policyTags, + }), + }); + + return acc; + }, []); + }, [canWriteTags, currentPolicyTag, hasDependentTags, isOffline, navigateToTagSettings, orderWeight, policyID, policyTags, showConfirmModal, translate, updateWorkspaceTagEnabled]); const tagListKeyedByName = useMemo( () => - filteredTagList.reduce>((acc, tag) => { + tagRows.reduce>((acc, tag) => { acc[tag.value] = tag; return acc; }, {}), - [filteredTagList], + [tagRows], ); if (!currentPolicyTag) { return ; } - const toggleTag = (tag: TagListItem) => { - setSelectedTags((prev) => { - if (prev.includes(tag.value)) { - return prev.filter((selectedTag) => selectedTag !== tag.value); - } - return [...prev, tag.value]; - }); - }; - - const toggleAllTags = () => { - const availableTags = filteredTagList.filter((tag) => tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - const anySelected = availableTags.some((tag) => selectedTags.includes(tag.value)); - - setSelectedTags(anySelected ? [] : availableTags.map((tag) => tag.value)); - }; - - const getCustomListHeader = () => { - if (filteredTagList.length === 0) { - return null; - } - return ( - - ); - }; - - const navigateToTagSettings = (tag: TagListItem) => { - if (!canWriteTags) { - return; - } - - const parentTagsFilter = tag?.rules?.parentTagsFilter; - const workspaceTagSettingsSuffix = parentTagsFilter - ? `${DYNAMIC_ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(orderWeight, tag.value)}?parentTagsFilter=${encodeURIComponent(parentTagsFilter)}` - : DYNAMIC_ROUTES.WORKSPACE_TAG_SETTINGS.getRoute(orderWeight, tag.value); - - Navigation.navigate(isQuickSettingsFlow ? createDynamicRoute(DYNAMIC_ROUTES.SETTINGS_TAG_SETTINGS.getRoute(orderWeight, tag.value)) : createDynamicRoute(workspaceTagSettingsSuffix)); - }; - const isLoading = !isOffline && policyTags === undefined; - const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'WorkspaceViewTagsPage', isOffline, isPolicyTagsUndefined: policyTags === undefined}; - - const listHeaderContent = - tagList.length >= CONST.STANDARD_LIST_ITEM_LIMIT ? ( - - ) : undefined; + const reasonAttributes: SkeletonSpanReasonAttributes = { + context: 'WorkspaceViewTagsPage', + isOffline, + isPolicyTagsUndefined: policyTags === undefined, + }; const getHeaderButtons = () => { if (!canWriteTags || (!isSmallScreenWidth && selectedTags.length === 0) || (isSmallScreenWidth && !isMobileSelectionModeEnabled)) { @@ -338,7 +282,6 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { return; } setSelectedTags([]); - // Disable the selected tags setWorkspaceTagEnabled(policyData, tagsToDisable, orderWeight); }, }); @@ -363,7 +306,9 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { shouldAlwaysShowDropdownMenu isSplitButton={false} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {count: selectedTags.length})} + customText={translate('workspace.common.selected', { + count: selectedTags.length, + })} options={options} style={[shouldDisplayButtonsInSeparateLine && styles.flexGrow1, shouldDisplayButtonsInSeparateLine && styles.mb3]} isDisabled={!selectedTags.length} @@ -389,6 +334,8 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { const selectionModeHeader = isMobileSelectionModeEnabled && isSmallScreenWidth; + const headerButtons = getHeaderButtons(); + return ( - {!shouldDisplayButtonsInSeparateLine && getHeaderButtons()} + {!shouldDisplayButtonsInSeparateLine && headerButtons} - {shouldDisplayButtonsInSeparateLine && !!getHeaderButtons() && {getHeaderButtons()}} + {shouldDisplayButtonsInSeparateLine && !!headerButtons && {headerButtons}} {!hasDependentTags && ( clearPolicyTagListErrorField({policyID, tagListIndex: orderWeight, errorField: 'required', policyTags})} + onCloseError={() => + clearPolicyTagListErrorField({ + policyID, + tagListIndex: orderWeight, + errorField: 'required', + policyTags, + }) + } disabled={!canWriteTags || (!currentPolicyTag?.required && !Object.values(currentPolicyTag?.tags ?? {}).some((tag) => tag.enabled))} disabledAction={withReadOnlyFallback()} showLockIcon={!canWriteTags || !isMultiLevelTags || isMakingLastRequiredTagListOptional(policy, policyTags, [currentPolicyTag])} @@ -458,7 +412,13 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { )} clearPolicyTagListErrors({policyID, tagListIndex: currentPolicyTag.orderWeight, policyTags})} + onClose={() => + clearPolicyTagListErrors({ + policyID, + tagListIndex: currentPolicyTag.orderWeight, + policyTags, + }) + } pendingAction={currentPolicyTag.pendingAction} errorRowStyles={styles.mh5} > @@ -477,26 +437,13 @@ function WorkspaceViewTagsPage({route}: WorkspaceViewTagsProps) { reasonAttributes={reasonAttributes} /> )} - {tagList.length > 0 && !isLoading && ( - 0 ? toggleAllTags : undefined} - onDismissError={(item) => clearPolicyTagErrors({policyID, tagName: item.value, tagListIndex: orderWeight, policyTags})} - shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()} - onTurnOnSelectionMode={(item) => item && canWriteTags && toggleTag(item)} - turnOnSelectionModeOnLongPress={canWriteTags && !hasDependentTags} - customListHeaderContent={listHeaderContent} - canSelectMultiple={canSelectMultiple} - selectAllAccessibilityLabel={translate('accessibilityHints.selectAllTags')} - onSelectRow={navigateToTagSettings} - shouldShowListEmptyContent={false} - onSelectionButtonPress={toggleTag} - shouldHeaderBeInsideList - shouldShowRightCaret={canWriteTags} - showScrollIndicator + {tagRows.length > 0 && !isLoading && ( + )} diff --git a/src/pages/workspace/tags/types.ts b/src/pages/workspace/tags/types.ts index ca0d161a5415..84eab4d92091 100644 --- a/src/pages/workspace/tags/types.ts +++ b/src/pages/workspace/tags/types.ts @@ -1,17 +1,5 @@ -import type {ListItem} from '@components/SelectionList/types'; - import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; -type TagListItem = ListItem & { - value: string; - enabled: boolean; - orderWeight?: number; - rules?: { - parentTagsFilter?: string; - }; - required?: boolean; -}; - type PolicyTag = { name: string; enabled: boolean; @@ -37,4 +25,4 @@ type PolicyTagList = { pendingAction?: PendingAction | null; }; -export type {TagListItem, PolicyTag, PolicyTagList}; +export type {PolicyTag, PolicyTagList};