From c2af527911282797e15292c077b4d33d9cff8390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Wed, 27 Mar 2024 17:39:05 +0100 Subject: [PATCH 1/7] refactorings --- .../SelectionList/BaseSelectionList.tsx | 3 + src/components/SelectionList/types.ts | 6 +- src/pages/WorkspaceSwitcherPage.tsx | 270 +++++++-------- src/pages/WorkspaceSwitcherPage_.tsx | 311 ++++++++++++++++++ .../card/WorkspaceCardCreateAWorkspace.tsx | 2 +- 5 files changed, 441 insertions(+), 151 deletions(-) create mode 100644 src/pages/WorkspaceSwitcherPage_.tsx diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 015fd284c0b4..96891b1cc07d 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -297,6 +297,9 @@ function BaseSelectionList( }; const renderSectionHeader = ({section}: {section: SectionListDataType}) => { + if (section.CustomSectionHeader) { + return ; + } if (!section.title || isEmptyObject(section.data)) { return null; } diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 8b070e1aa5cb..108a62fff215 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -324,7 +324,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/pages/WorkspaceSwitcherPage.tsx b/src/pages/WorkspaceSwitcherPage.tsx index 2eb5ecaf373f..68cdffbaf57e 100644 --- a/src/pages/WorkspaceSwitcherPage.tsx +++ b/src/pages/WorkspaceSwitcherPage.tsx @@ -1,19 +1,18 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useMemo} 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 OptionsSelector from '@components/OptionsSelector'; 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 useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; @@ -53,13 +52,49 @@ type WorkspaceSwitcherPageOnyxProps = { type WorkspaceSwitcherPageProps = WorkspaceSwitcherPageOnyxProps; +function WorkspacesSectionHeader() { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + {translate('common.workspaces')} + + + + { + Navigation.goBack(); + interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()); + }} + > + {({hovered}) => ( + + )} + + + + ); +} function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); - const [selectedOption, setSelectedOption] = useState(); - const [searchTerm, setSearchTerm] = useState(''); - const {inputCallbackRef} = useAutoFocusInput(); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const {translate} = useLocalize(); const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); @@ -105,11 +140,6 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { const {policyID} = option; - if (policyID) { - setSelectedOption(option); - } else { - setSelectedOption(undefined); - } setActiveWorkspaceID(policyID); Navigation.goBack(); if (policyID !== activeWorkspaceID) { @@ -141,163 +171,105 @@ function WorkspaceSwitcherPage({policies}: WorkspaceSwitcherPageProps) { boldStyle: hasUnreadData(policy?.id), keyForList: policy?.id, isPolicyAdmin: PolicyUtils.isPolicyAdmin(policy), + isSelected: activeWorkspaceID === policy?.id, })); - }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline]); + }, [policies, isOffline, getIndicatorTypeForPolicy, hasUnreadData, activeWorkspaceID]); const filteredAndSortedUserWorkspaces = useMemo( () => usersWorkspaces - .filter((policy) => policy.text?.toLowerCase().includes(searchTerm?.toLowerCase() ?? '')) + .filter((policy) => policy.text?.toLowerCase().includes(debouncedSearchTerm?.toLowerCase() ?? '')) .sort((policy1, policy2) => sortWorkspacesBySelected(policy1, policy2, activeWorkspaceID)), - [searchTerm, usersWorkspaces, activeWorkspaceID], - ); - - const usersWorkspacesSectionData = useMemo( - () => ({ - data: filteredAndSortedUserWorkspaces, - shouldShow: true, - indexOffset: 0, - }), - [filteredAndSortedUserWorkspaces], + [debouncedSearchTerm, usersWorkspaces, activeWorkspaceID], ); - 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 usersWorkspacesSectionData = useMemo(() => { + const options = [ + { + 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), + boldStyle: hasUnreadData(undefined), + isSelected: activeWorkspaceID === undefined, + }, + ], + }, + ]; + if (filteredAndSortedUserWorkspaces.length > 0) { + options.push({ + CustomSectionHeader: WorkspacesSectionHeader, + data: filteredAndSortedUserWorkspaces, + shouldShow: true, + indexOffset: 1, + }); + } + return options; + }, [activeWorkspaceID, filteredAndSortedUserWorkspaces, getIndicatorTypeForPolicy, hasUnreadData, translate]); const headerMessage = filteredAndSortedUserWorkspaces.length === 0 ? translate('common.noResultsFound') : ''; + const shouldShowCreateWorkspace = usersWorkspaces.length === 0; - const workspacesSection = useMemo( - () => ( - <> - 0 ? [styles.mb1] : [styles.mb3])]}> - - - {translate('common.workspaces')} - + const renderRightHandSideComponent = useCallback( + (item: (typeof filteredAndSortedUserWorkspaces)[number]) => { + if (item.isSelected) { + return ( + + - - { - Navigation.goBack(); - interceptAnonymousUser(() => App.createWorkspaceWithPolicyDraftAndNavigateToIt()); - }} - > - {({hovered}) => ( - - )} - - - + ); + } - {usersWorkspaces.length > 0 ? ( - = CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH} - onChangeText={setSearchTerm} - selectedOptions={selectedOption ? [selectedOption] : []} - onSelectRow={selectPolicy} - shouldPreventDefaultFocusOnSelectRow - headerMessage={headerMessage} - highlightSelectedOptions - shouldShowOptions - autoFocus={false} - canSelectMultipleOptions={false} - shouldShowSubscript={false} - showTitleTooltip={false} - contentContainerStyles={[styles.pt0, styles.mt0]} - textIconLeft={MagnifyingGlass} - // Null is to avoid selecting unfocused option when Global selected, undefined is to focus selected workspace - initiallyFocusedOptionKey={!activeWorkspaceID ? null : undefined} - /> - ) : ( - - )} - - ), - [ - inputCallbackRef, - setSearchTerm, - searchTerm, - selectPolicy, - selectedOption, - styles, - theme.textSupporting, - translate, - usersWorkspaces.length, - usersWorkspacesSectionData, - activeWorkspaceID, - theme.icon, - headerMessage, - ], + if (item.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) { + return ( + + + + ); + } + if (item.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) { + return ( + + + + ); + } + return null; + }, + [styles, theme], ); - useEffect(() => { - if (!activeWorkspaceID) { - return; - } - const optionToSet = usersWorkspaces.find((option) => option.policyID === activeWorkspaceID); - setSelectedOption(optionToSet); - }, [activeWorkspaceID, usersWorkspaces]); - return ( - {everythingSection} - {workspacesSection} + = CONST.WORKSPACE_SWITCHER.MINIMUM_WORKSPACES_TO_SHOW_SEARCH ? translate('common.search') : undefined} + textInputValue={searchTerm} + onChangeText={setSearchTerm} + headerMessage={headerMessage} + rightHandSideComponent={renderRightHandSideComponent} + footerContent={shouldShowCreateWorkspace && } + /> ); } diff --git a/src/pages/WorkspaceSwitcherPage_.tsx b/src/pages/WorkspaceSwitcherPage_.tsx new file mode 100644 index 000000000000..2eb5ecaf373f --- /dev/null +++ b/src/pages/WorkspaceSwitcherPage_.tsx @@ -0,0 +1,311 @@ +import React, {useCallback, useEffect, 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 OptionsSelector from '@components/OptionsSelector'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import ScreenWrapper from '@components/ScreenWrapper'; +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 [selectedOption, setSelectedOption] = useState(); + 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; + + if (policyID) { + setSelectedOption(option); + } else { + setSelectedOption(undefined); + } + 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), + })); + }, [policies, getIndicatorTypeForPolicy, hasUnreadData, isOffline]); + + 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, + indexOffset: 0, + }), + [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} + onChangeText={setSearchTerm} + selectedOptions={selectedOption ? [selectedOption] : []} + onSelectRow={selectPolicy} + shouldPreventDefaultFocusOnSelectRow + headerMessage={headerMessage} + highlightSelectedOptions + shouldShowOptions + autoFocus={false} + canSelectMultipleOptions={false} + shouldShowSubscript={false} + showTitleTooltip={false} + contentContainerStyles={[styles.pt0, styles.mt0]} + textIconLeft={MagnifyingGlass} + // Null is to avoid selecting unfocused option when Global selected, undefined is to focus selected workspace + initiallyFocusedOptionKey={!activeWorkspaceID ? null : undefined} + /> + ) : ( + + )} + + ), + [ + inputCallbackRef, + setSearchTerm, + searchTerm, + selectPolicy, + selectedOption, + styles, + theme.textSupporting, + translate, + usersWorkspaces.length, + usersWorkspacesSectionData, + activeWorkspaceID, + theme.icon, + headerMessage, + ], + ); + + useEffect(() => { + if (!activeWorkspaceID) { + return; + } + const optionToSet = usersWorkspaces.find((option) => option.policyID === activeWorkspaceID); + setSelectedOption(optionToSet); + }, [activeWorkspaceID, usersWorkspaces]); + + return ( + + + {everythingSection} + {workspacesSection} + + ); +} + +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..2c0c039718e2 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.mh0]} >