diff --git a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx index c7816b710692..6170b81073a2 100644 --- a/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx +++ b/src/components/DatePicker/CalendarPicker/YearPickerModal.tsx @@ -34,7 +34,7 @@ function YearPickerModal({isVisible, years, currentYear = new Date().getFullYear const yearsList = searchText === '' ? years : years.filter((year) => year.text?.includes(searchText)); return { headerMessage: !yearsList.length ? translate('common.noResultsFound') : '', - sections: [{data: yearsList.sort((a, b) => b.value - a.value)}], + sections: [{data: yearsList.sort((a, b) => b.value - a.value), indexOffset: 0}], }; }, [years, searchText, translate]); diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index 436f4c147931..7bbd3e344c3f 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -184,7 +184,7 @@ function BaseOptionsList( option={item} showTitleTooltip={showTitleTooltip} hoverStyle={optionHoveredStyle} - optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + section.indexOffset} + optionIsFocused={!disableFocusOptions && !isItemDisabled && focusedIndex === index + (section.indexOffset ?? 0)} onSelectRow={onSelectRow} isSelected={isSelected} showSelectedState={canSelectMultipleOptions} diff --git a/src/components/OptionsList/types.ts b/src/components/OptionsList/types.ts index b7180e6281b4..7f23da965f39 100644 --- a/src/components/OptionsList/types.ts +++ b/src/components/OptionsList/types.ts @@ -22,7 +22,7 @@ type Section = { type SectionWithIndexOffset = Section & { /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: number; + indexOffset?: number; }; type OptionsListProps = { diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 62f098e76228..a5bdf46450ae 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -2,7 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import isEmpty from 'lodash/isEmpty'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native'; +import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListData, SectionListRenderItemInfo} from 'react-native'; import {View} from 'react-native'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; @@ -52,6 +52,7 @@ function BaseSelectionList( onConfirm, headerContent, footerContent, + listFooterContent, showScrollIndicator = true, showLoadingPlaceholder = false, showConfirmButton = false, @@ -294,7 +295,7 @@ function BaseSelectionList( * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] */ - const getItemLayout = (data: Array> | null, flatDataArrayIndex: number) => { + const getItemLayout = (data: Array>> | null, flatDataArrayIndex: number) => { const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex]; if (!targetItem) { @@ -313,6 +314,10 @@ function BaseSelectionList( }; const renderSectionHeader = ({section}: {section: SectionListDataType}) => { + if (section.CustomSectionHeader) { + return ; + } + if (!section.title || isEmptyObject(section.data)) { return null; } @@ -329,7 +334,7 @@ function BaseSelectionList( }; const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { - const normalizedIndex = index + section.indexOffset; + const normalizedIndex = index + (section?.indexOffset ?? 0); const isDisabled = !!section.isDisabled || item.isDisabled; const isItemFocused = !isDisabled && (focusedIndex === normalizedIndex || itemsToHighlight?.has(item.keyForList ?? '')); // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. @@ -603,7 +608,7 @@ function BaseSelectionList( testID="selection-list" onLayout={onSectionListLayout} style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0} - ListFooterComponent={ShowMoreButtonInstance} + ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance} /> {children} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 119634c61fd8..9d1d9e15ca0b 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -12,6 +12,11 @@ import type RadioListItem from './RadioListItem'; import type TableListItem from './TableListItem'; import type UserListItem from './UserListItem'; +type TRightHandSideComponent = { + /** Component to display on the right side */ + rightHandSideComponent?: ((item: TItem) => ReactElement | null | undefined) | ReactElement | null; +}; + type CommonListItemProps = { /** Whether this item is focused (for arrow key controls) */ isFocused?: boolean; @@ -34,9 +39,6 @@ type CommonListItemProps = { /** Callback to fire when an error is dismissed */ onDismissError?: (item: TItem) => void; - /** Component to display on the right side */ - rightHandSideComponent?: ((item: TItem) => ReactElement | null) | ReactElement | null; - /** Styles for the pressable component */ pressableStyle?: StyleProp; @@ -54,7 +56,7 @@ type CommonListItemProps = { /** Handles what to do when the item is focused */ onFocus?: () => void; -}; +} & TRightHandSideComponent; type ListItem = { /** Text to display */ @@ -185,12 +187,12 @@ type Section = { type SectionWithIndexOffset = Section & { /** The initial index of this section given the total number of options in each section's data array */ - indexOffset: number; + indexOffset?: number; }; type BaseSelectionListProps = Partial & { /** Sections for the section list */ - sections: Array>> | typeof CONST.EMPTY_ARRAY; + sections: Array> | typeof CONST.EMPTY_ARRAY; /** Default renderer for every item in the list */ ListItem: ValidListItem; @@ -282,6 +284,9 @@ type BaseSelectionListProps = Partial & { /** Custom content to display in the footer */ footerContent?: ReactNode; + /** Custom content to display in the footer of list component. If present ShowMore button won't be displayed */ + listFooterContent?: React.JSX.Element | null; + /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */ shouldUseDynamicMaxToRenderPerBatch?: boolean; @@ -294,9 +299,6 @@ type BaseSelectionListProps = Partial & { /** Whether focus event should be delayed */ shouldDelayFocus?: boolean; - /** Component to display on the right side of each child */ - rightHandSideComponent?: ((item: TItem) => ReactElement | null) | ReactElement | null; - /** Whether to show the loading indicator for new options */ isLoadingNewOptions?: boolean; @@ -323,7 +325,7 @@ type BaseSelectionListProps = Partial & { * When false, the list will render immediately and scroll to the bottom which works great for small lists. */ shouldHideListOnInitialRender?: boolean; -}; +} & TRightHandSideComponent; type SelectionListHandle = { scrollAndHighlightItem?: (items: string[], timeout: number) => void; @@ -344,7 +346,11 @@ type FlattenedSectionsReturn = { type ButtonOrCheckBoxRoles = 'button' | 'checkbox'; -type SectionListDataType = SectionListData>; +type ExtendedSectionListData> = SectionListData & { + CustomSectionHeader?: ({section}: {section: TSection}) => ReactElement; +}; + +type SectionListDataType = ExtendedSectionListData>; export type { BaseSelectionListProps, diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index aa16d7b2dc5a..1e76b19aa99c 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -117,6 +117,7 @@ type TaxSection = { type CategoryTreeSection = CategorySectionBase & { data: OptionTree[]; + indexOffset?: number; }; type Category = { @@ -1023,11 +1024,13 @@ function getCategoryListSections( const numberOfEnabledCategories = enabledCategories.length; if (numberOfEnabledCategories === 0 && selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptions, true); categorySections.push({ // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptions, true), + data, + indexOffset: data.length, }); return categorySections; @@ -1046,22 +1049,26 @@ function getCategoryListSections( }); }); + const data = getCategoryOptionTree(searchCategories, true); categorySections.push({ // "Search" section title: '', shouldShow: true, - data: getCategoryOptionTree(searchCategories, true), + data, + indexOffset: data.length, }); return categorySections; } if (selectedOptions.length > 0) { + const data = getCategoryOptionTree(selectedOptions, true); categorySections.push({ // "Selected" section title: '', shouldShow: false, - data: getCategoryOptionTree(selectedOptions, true), + data, + indexOffset: data.length, }); } @@ -1069,11 +1076,13 @@ function getCategoryListSections( const filteredCategories = enabledCategories.filter((category) => !selectedOptionNames.includes(category.name)); if (numberOfEnabledCategories < CONST.CATEGORY_LIST_THRESHOLD) { + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames); categorySections.push({ // "All" section when items amount less than the threshold title: '', shouldShow: false, - data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), + data, + indexOffset: data.length, }); return categorySections; @@ -1089,19 +1098,23 @@ function getCategoryListSections( if (filteredRecentlyUsedCategories.length > 0) { const cutRecentlyUsedCategories = filteredRecentlyUsedCategories.slice(0, maxRecentReportsToShow); + const data = getCategoryOptionTree(cutRecentlyUsedCategories, true); categorySections.push({ // "Recent" section title: Localize.translateLocal('common.recent'), shouldShow: true, - data: getCategoryOptionTree(cutRecentlyUsedCategories, true), + data, + indexOffset: data.length, }); } + const data = getCategoryOptionTree(filteredCategories, false, selectedOptionNames); categorySections.push({ // "All" section when items amount more than the threshold title: Localize.translateLocal('common.all'), shouldShow: true, - data: getCategoryOptionTree(filteredCategories, false, selectedOptionNames), + data, + indexOffset: data.length, }); return categorySections; @@ -2356,4 +2369,4 @@ export { getFirstKeyForList, }; -export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option}; +export type {MemberForList, CategorySection, CategoryTreeSection, Options, OptionList, SearchOption, PayeePersonalDetails, Category, TaxRatesOption, Option, OptionTree}; diff --git a/src/libs/getSectionsWithIndexOffset.ts b/src/libs/getSectionsWithIndexOffset.ts index 7de78d048a4d..3237651a0385 100644 --- a/src/libs/getSectionsWithIndexOffset.ts +++ b/src/libs/getSectionsWithIndexOffset.ts @@ -3,7 +3,7 @@ import type {SectionListData} from 'react-native'; /** * Returns a list of sections with indexOffset */ -export default function getSectionsWithIndexOffset(sections: Array>): Array> { +export default function getSectionsWithIndexOffset(sections: Array>): Array> { return sections.map((section, index) => { const indexOffset = [...sections].splice(0, index).reduce((acc, curr) => acc + (curr.data?.length ?? 0), 0); return {...section, indexOffset}; diff --git a/src/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx deleted file mode 100644 index f1a439548f1b..000000000000 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import React, {useCallback, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import {MagnifyingGlass} from '@components/Icon/Expensicons'; -import OptionRow from '@components/OptionRow'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import UserListItem from '@components/SelectionList/UserListItem'; -import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; -import useActiveWorkspace from '@hooks/useActiveWorkspace'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesSettingsUtils'; -import * as App from '@userActions/App'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy} from '@src/types/onyx'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import WorkspaceCardCreateAWorkspace from './workspace/card/WorkspaceCardCreateAWorkspace'; - -type SimpleWorkspaceItem = { - text?: string; - policyID?: string; - isPolicyAdmin?: boolean; -}; - -const sortWorkspacesBySelected = (workspace1: SimpleWorkspaceItem, workspace2: SimpleWorkspaceItem, selectedWorkspaceID: string | undefined): number => { - if (workspace1.policyID === selectedWorkspaceID) { - return -1; - } - if (workspace2.policyID === selectedWorkspaceID) { - return 1; - } - return workspace1.text?.toLowerCase().localeCompare(workspace2.text?.toLowerCase() ?? '') ?? 0; -}; - -type WorkspaceSwitcherPageOnyxProps = { - /** The list of this user's policies */ - policies: OnyxCollection; -}; - -type WorkspaceSwitcherPageProps = WorkspaceSwitcherPageOnyxProps; - -function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {isOffline} = useNetwork(); - const [searchTerm, setSearchTerm] = useState(''); - const {inputCallbackRef} = useAutoFocusInput(); - const {translate} = useLocalize(); - const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); - - const brickRoadsForPolicies = useMemo(() => getWorkspacesBrickRoads(), []); - const unreadStatusesForPolicies = useMemo(() => getWorkspacesUnreadStatuses(), []); - - const getIndicatorTypeForPolicy = useCallback( - (policyId?: string) => { - if (policyId && policyId !== activeWorkspaceID) { - return brickRoadsForPolicies[policyId]; - } - - if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR)) { - return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; - } - - if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO)) { - return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; - } - - return undefined; - }, - [activeWorkspaceID, brickRoadsForPolicies], - ); - - const hasUnreadData = useCallback( - // TO DO: Implement checking if policy has some unread data - (policyId?: string) => { - if (policyId) { - return unreadStatusesForPolicies[policyId]; - } - - return Object.values(unreadStatusesForPolicies).some((status) => status); - }, - [unreadStatusesForPolicies], - ); - - const selectPolicy = useCallback( - (option?: SimpleWorkspaceItem) => { - if (!option) { - return; - } - - const {policyID} = option; - - setActiveWorkspaceID(policyID); - - if (policyID !== activeWorkspaceID) { - Navigation.navigateWithSwitchPolicyID({policyID}); - } else { - Navigation.goBack(); - } - }, - [activeWorkspaceID, setActiveWorkspaceID], - ); - - const usersWorkspaces = useMemo(() => { - if (!policies || isEmptyObject(policies)) { - return []; - } - - return Object.values(policies) - .filter((policy) => PolicyUtils.shouldShowPolicy(policy, !!isOffline)) - .map((policy) => ({ - text: policy?.name, - policyID: policy?.id, - brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id), - icons: [ - { - source: policy?.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy?.name), - fallbackIcon: Expensicons.FallbackWorkspaceAvatar, - name: policy?.name, - type: CONST.ICON_TYPE_WORKSPACE, - }, - ], - boldStyle: hasUnreadData(policy?.id), - keyForList: policy?.id, - isPolicyAdmin: PolicyUtils.isPolicyAdmin(policy), - isSelected: policy?.id === activeWorkspaceID, - })); - }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline, activeWorkspaceID]); - - const filteredAndSortedUserWorkspaces = useMemo( - () => - usersWorkspaces - .filter((policy) => policy.text?.toLowerCase().includes(searchTerm?.toLowerCase() ?? '')) - .sort((policy1, policy2) => sortWorkspacesBySelected(policy1, policy2, activeWorkspaceID)), - [searchTerm, usersWorkspaces, activeWorkspaceID], - ); - - const usersWorkspacesSectionData = useMemo( - () => ({ - data: filteredAndSortedUserWorkspaces, - shouldShow: true, - }), - [filteredAndSortedUserWorkspaces], - ); - - const everythingSection = useMemo(() => { - const option = { - reportID: '', - text: CONST.WORKSPACE_SWITCHER.NAME, - icons: [ - { - source: Expensicons.ExpensifyAppIcon, - name: CONST.WORKSPACE_SWITCHER.NAME, - type: CONST.ICON_TYPE_AVATAR, - }, - ], - brickRoadIndicator: getIndicatorTypeForPolicy(undefined), - boldStyle: hasUnreadData(undefined), - }; - - return ( - <> - - - {translate('workspace.switcher.everythingSection')} - - - - - - - ); - }, [activeWorkspaceID, getIndicatorTypeForPolicy, hasUnreadData, selectPolicy, styles, theme.textSupporting, translate]); - - const headerMessage = filteredAndSortedUserWorkspaces.length === 0 ? translate('common.noResultsFound') : ''; - - const workspacesSection = useMemo( - () => ( - <> - 0 ? [styles.mb1] : [styles.mb3])]}> - - - {translate('common.workspaces')} - - - - { - Navigation.goBack(); - interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()); - }} - > - {({hovered}) => ( - - )} - - - - - {usersWorkspaces.length > 0 ? ( - = CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? MagnifyingGlass : undefined} - initiallyFocusedOptionKey={activeWorkspaceID} - textInputAutoFocus={false} - /> - ) : ( - - )} - - ), - [ - inputCallbackRef, - setSearchTerm, - searchTerm, - selectPolicy, - styles, - theme.textSupporting, - translate, - usersWorkspaces.length, - usersWorkspacesSectionData, - activeWorkspaceID, - theme.icon, - headerMessage, - ], - ); - - return ( - - - {everythingSection} - {workspacesSection} - - ); -} - -WorkspaceSwitcherPage.displayName = 'WorkspaceSwitcherPage'; - -export default withOnyx({ - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - }, -})(WorkspaceSwitcherPage); diff --git a/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx b/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx new file mode 100644 index 000000000000..85e13ba4c0a5 --- /dev/null +++ b/src/pages/WorkspaceSwitcherPage/WorkspacesSectionHeader.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import {View} from 'react-native'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import {PressableWithFeedback} from '@components/Pressable'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import * as App from '@userActions/App'; +import CONST from '@src/CONST'; + +function WorkspacesSectionHeader() { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + {translate('common.workspaces')} + + + + { + Navigation.goBack(); + interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()); + }} + > + {({hovered}) => ( + + )} + + + + ); +} + +export default WorkspacesSectionHeader; diff --git a/src/pages/WorkspaceSwitcherPage/index.tsx b/src/pages/WorkspaceSwitcherPage/index.tsx new file mode 100644 index 000000000000..489c9566d6c7 --- /dev/null +++ b/src/pages/WorkspaceSwitcherPage/index.tsx @@ -0,0 +1,208 @@ +import React, {useCallback, useMemo} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import type {ListItem, SectionListDataType} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useActiveWorkspace from '@hooks/useActiveWorkspace'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesSettingsUtils'; +import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; +import WorkspaceCardCreateAWorkspace from '@pages/workspace/card/WorkspaceCardCreateAWorkspace'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import WorkspacesSectionHeader from './WorkspacesSectionHeader'; + +type WorkspaceListItem = { + text: string; + policyID: string; + isPolicyAdmin?: boolean; + brickRoadIndicator?: BrickRoad; +} & ListItem; + +const sortWorkspacesBySelected = (workspace1: WorkspaceListItem, workspace2: WorkspaceListItem, selectedWorkspaceID: string | undefined): number => { + if (workspace1.policyID === selectedWorkspaceID) { + return -1; + } + if (workspace2.policyID === selectedWorkspaceID) { + return 1; + } + return workspace1.text?.toLowerCase().localeCompare(workspace2.text?.toLowerCase() ?? '') ?? 0; +}; + +type WorkspaceSwitcherPageOnyxProps = { + /** The list of this user's policies */ + policies: OnyxCollection; +}; + +type WorkspaceSwitcherPageProps = WorkspaceSwitcherPageOnyxProps; + +const WorkspaceCardCreateAWorkspaceInstance = ; + +function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { + const {isOffline} = useNetwork(); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const {translate} = useLocalize(); + const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); + + const brickRoadsForPolicies = useMemo(() => getWorkspacesBrickRoads(), []); + const unreadStatusesForPolicies = useMemo(() => getWorkspacesUnreadStatuses(), []); + + const getIndicatorTypeForPolicy = useCallback( + (policyId?: string) => { + if (policyId && policyId !== activeWorkspaceID) { + return brickRoadsForPolicies[policyId]; + } + + if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR)) { + return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + } + + if (Object.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD_INDICATOR_STATUS.INFO)) { + return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; + } + + return undefined; + }, + [activeWorkspaceID, brickRoadsForPolicies], + ); + + const hasUnreadData = useCallback( + // TO DO: Implement checking if policy has some unread data + (policyId?: string) => { + if (policyId) { + return unreadStatusesForPolicies[policyId]; + } + + return Object.values(unreadStatusesForPolicies).some((status) => status); + }, + [unreadStatusesForPolicies], + ); + + const selectPolicy = useCallback( + (option?: WorkspaceListItem) => { + if (!option) { + return; + } + + const {policyID} = option; + + setActiveWorkspaceID(policyID); + Navigation.goBack(); + if (policyID !== activeWorkspaceID) { + Navigation.navigateWithSwitchPolicyID({policyID}); + } + }, + [activeWorkspaceID, setActiveWorkspaceID], + ); + + const usersWorkspaces = useMemo(() => { + if (!policies || isEmptyObject(policies)) { + return []; + } + + return Object.values(policies) + .filter((policy) => PolicyUtils.shouldShowPolicy(policy, !!isOffline)) + .map((policy) => ({ + text: policy?.name ?? '', + policyID: policy?.id ?? '', + brickRoadIndicator: getIndicatorTypeForPolicy(policy?.id), + icons: [ + { + source: policy?.avatar ? policy.avatar : ReportUtils.getDefaultWorkspaceAvatar(policy?.name), + fallbackIcon: Expensicons.FallbackWorkspaceAvatar, + name: policy?.name, + type: CONST.ICON_TYPE_WORKSPACE, + }, + ], + boldStyle: hasUnreadData(policy?.id), + keyForList: policy?.id, + isPolicyAdmin: PolicyUtils.isPolicyAdmin(policy), + isSelected: activeWorkspaceID === policy?.id, + })); + }, [policies, isOffline, getIndicatorTypeForPolicy, hasUnreadData, activeWorkspaceID]); + + const filteredAndSortedUserWorkspaces = useMemo( + () => + usersWorkspaces + .filter((policy) => policy.text?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase() ?? '')) + .sort((policy1, policy2) => sortWorkspacesBySelected(policy1, policy2, activeWorkspaceID)), + [debouncedSearchTerm, usersWorkspaces, activeWorkspaceID], + ); + + const sections = useMemo(() => { + const options: Array> = [ + { + title: translate('workspace.switcher.everythingSection'), + shouldShow: true, + indexOffset: 0, + data: [ + { + text: CONST.WORKSPACE_SWITCHER.NAME, + policyID: '', + icons: [{source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR}], + brickRoadIndicator: getIndicatorTypeForPolicy(undefined), + isSelected: activeWorkspaceID === undefined, + keyForList: CONST.WORKSPACE_SWITCHER.NAME, + }, + ], + }, + ]; + options.push({ + CustomSectionHeader: WorkspacesSectionHeader, + data: filteredAndSortedUserWorkspaces, + shouldShow: true, + indexOffset: 1, + }); + return options; + }, [activeWorkspaceID, filteredAndSortedUserWorkspaces, getIndicatorTypeForPolicy, translate]); + + const headerMessage = filteredAndSortedUserWorkspaces.length === 0 && usersWorkspaces.length ? translate('common.noResultsFound') : ''; + const shouldShowCreateWorkspace = usersWorkspaces.length === 0; + + return ( + + {({didScreenTransitionEnd}) => ( + <> + + + ListItem={UserListItem} + sections={didScreenTransitionEnd ? sections : CONST.EMPTY_ARRAY} + onSelectRow={selectPolicy} + textInputLabel={usersWorkspaces.length >= CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? translate('common.search') : undefined} + textInputValue={searchTerm} + onChangeText={setSearchTerm} + headerMessage={headerMessage} + listFooterContent={shouldShowCreateWorkspace ? WorkspaceCardCreateAWorkspaceInstance : null} + initiallyFocusedOptionKey={activeWorkspaceID ?? CONST.WORKSPACE_SWITCHER.NAME} + showLoadingPlaceholder + /> + + )} + + ); +} + +WorkspaceSwitcherPage.displayName = 'WorkspaceSwitcherPage'; + +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(WorkspaceSwitcherPage); diff --git a/src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx b/src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx index 3546a437b2e2..f965a5bbba30 100644 --- a/src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx +++ b/src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx @@ -17,7 +17,7 @@ function WorkspaceCardCreateAWorkspace() { cardLayout={CARD_LAYOUT.ICON_ON_TOP} subtitle={translate('workspace.emptyWorkspace.subtitle')} subtitleMuted - containerStyles={[styles.highlightBG]} + containerStyles={[styles.highlightBG, styles.mv2]} >