From 3566460ccd2aa92e0f78a9b82d31a21f2eb62016 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 20 Mar 2024 00:45:01 +0100 Subject: [PATCH 001/206] add multitag regex --- src/CONST.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 3c53f083abac..a3fd483255a4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1655,6 +1655,10 @@ const CONST = { MERGED_ACCOUNT_PREFIX: /^(MERGED_\d+@)/, + get MULTI_LEVEL_TAG() { + return new RegExp("/\\{1,2}:/g"); + }, + ROUTES: { VALIDATE_LOGIN: /\/v($|(\/\/*))/, UNLINK_LOGIN: /\/u($|(\/\/*))/, From 04d2b5962b3d77bc25b65dc0053585427163658e Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 20 Mar 2024 00:59:48 +0100 Subject: [PATCH 002/206] introducted a new boolean agrument, defaulted to false, callled excludeMultiLevelTags for the getTagLists function --- src/libs/PolicyUtils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 39e6c8932aad..45e5aa03760f 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -188,13 +188,15 @@ function getTagListName(policyTagList: OnyxEntry, tagIndex: numbe /** * Gets all tag lists of a policy */ -function getTagLists(policyTagList: OnyxEntry): Array { +function getTagLists(policyTagList: OnyxEntry, excludeMultiLevelTags = false): Array { if (isEmptyObject(policyTagList)) { return []; } + const regex = new RegExp(CONST.REGEX.MULTI_LEVEL_TAG, `/\\{1,2}:/g`); + return Object.values(policyTagList) - .filter((policyTagListValue) => policyTagListValue !== null) + .filter((policyTagListValue) => policyTagListValue !== null && (!excludeMultiLevelTags || regex.test(policyTagListValue.name))) .sort((tagA, tagB) => tagA.orderWeight - tagB.orderWeight); } From cbd67a509ec7230569bf924dec7a47bea45647bf Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 20 Mar 2024 01:31:18 +0100 Subject: [PATCH 003/206] introducted a new boolean agrument, defaulted to false, callled excludeMultiLevelTags for the getTagLists function --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 45e5aa03760f..7a34ae22f023 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -196,7 +196,7 @@ function getTagLists(policyTagList: OnyxEntry, excludeMultiLevelT const regex = new RegExp(CONST.REGEX.MULTI_LEVEL_TAG, `/\\{1,2}:/g`); return Object.values(policyTagList) - .filter((policyTagListValue) => policyTagListValue !== null && (!excludeMultiLevelTags || regex.test(policyTagListValue.name))) + .filter((policyTagListValue) => !(!policyTagListValue || excludeMultiLevelTags || regex.test(policyTagListValue.name))) .sort((tagA, tagB) => tagA.orderWeight - tagB.orderWeight); } From 3dc48b4e97f9427f0d459a6da5fdce76df758e77 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 21 Mar 2024 15:55:25 +0300 Subject: [PATCH 004/206] Removing multitags --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 392d392c55ef..860b47d615ec 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -76,6 +76,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { policyTagLists .map((policyTagList) => Object.values(policyTagList.tags || []) + .filter((value) => value.name == getCleanTagName(value.name)) .sort((a, b) => localeCompare(a.name, b.name)) .map((value) => ({ value: value.name, From 3c73312640b3fe60b3d050a98ab76d60754b7b5f Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 21 Mar 2024 14:15:47 +0100 Subject: [PATCH 005/206] fix --- src/libs/PolicyUtils.ts | 34 +++---------------- .../workspace/tags/WorkspaceTagsPage.tsx | 2 +- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7a34ae22f023..f7f83f1cfbcb 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -4,8 +4,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx'; -import type {PolicyFeatureName} from '@src/types/onyx/Policy'; +import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Navigation from './Navigation/Navigation'; @@ -35,7 +34,7 @@ function hasPolicyMemberError(policyMembers: OnyxEntry): boolean * Check if the policy has any tax rate errors. */ function hasTaxRateError(policy: OnyxEntry): boolean { - return Object.values(policy?.taxRates?.taxes ?? {}).some((taxRate) => Object.keys(taxRate?.errors ?? {}).length > 0 || Object.values(taxRate?.errorFields ?? {}).some(Boolean)); + return Object.values(policy?.taxRates?.taxes ?? {}).some((taxRate) => Object.keys(taxRate?.errors ?? {}).length > 0); } /** @@ -188,15 +187,13 @@ function getTagListName(policyTagList: OnyxEntry, tagIndex: numbe /** * Gets all tag lists of a policy */ -function getTagLists(policyTagList: OnyxEntry, excludeMultiLevelTags = false): Array { +function getTagLists(policyTagList: OnyxEntry ): Array { if (isEmptyObject(policyTagList)) { return []; } - const regex = new RegExp(CONST.REGEX.MULTI_LEVEL_TAG, `/\\{1,2}:/g`); - return Object.values(policyTagList) - .filter((policyTagListValue) => !(!policyTagListValue || excludeMultiLevelTags || regex.test(policyTagListValue.name))) + .filter((policyTagListValue) => policyTagListValue !== null) .sort((tagA, tagB) => tagA.orderWeight - tagB.orderWeight); } @@ -279,26 +276,6 @@ function goBackFromInvalidPolicy() { Navigation.navigate(ROUTES.SETTINGS_WORKSPACES); } -/** Get a tax with given ID from policy */ -function getTaxByID(policy: OnyxEntry, taxID: string): TaxRate | undefined { - return policy?.taxRates?.taxes?.[taxID]; -} - -/** - * Whether the tax rate can be deleted and disabled - */ -function canEditTaxRate(policy: Policy, taxID: string): boolean { - return policy.taxRates?.defaultExternalID !== taxID; -} - -function isPolicyFeatureEnabled(policy: OnyxEntry | EmptyObject, featureName: PolicyFeatureName): boolean { - if (featureName === CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED) { - return Boolean(policy?.tax?.trackingEnabled); - } - - return Boolean(policy?.[featureName]); -} - export { getActivePolicies, hasAccountingConnections, @@ -319,7 +296,6 @@ export { getIneligibleInvitees, getTagLists, getTagListName, - canEditTaxRate, getTagList, getCleanedTagName, getCountOfEnabledTagsOfList, @@ -330,9 +306,7 @@ export { getPathWithoutPolicyID, getPolicyMembersByIdWithoutCurrentUser, goBackFromInvalidPolicy, - isPolicyFeatureEnabled, hasTaxRateError, - getTaxByID, hasPolicyCategoriesError, }; diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index d19604608bbb..0b0758affd93 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -84,7 +84,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { policyTagLists .map((policyTagList) => Object.values(policyTagList.tags || []) - .filter((value) => value.name == getCleanTagName(value.name)) + .filter((value) => value.name === PolicyUtils.getCleanedTagName(value.name)) .sort((a, b) => localeCompare(a.name, b.name)) .map((value) => ({ value: value.name, From 6dbefcfad1a8b0189754aaee634b87d51cb59a51 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 21 Mar 2024 15:31:28 +0100 Subject: [PATCH 006/206] fix --- src/libs/PolicyUtils.ts | 2 +- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index f7f83f1cfbcb..675e268045c1 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -187,7 +187,7 @@ function getTagListName(policyTagList: OnyxEntry, tagIndex: numbe /** * Gets all tag lists of a policy */ -function getTagLists(policyTagList: OnyxEntry ): Array { +function getTagLists(policyTagList: OnyxEntry): Array { if (isEmptyObject(policyTagList)) { return []; } diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 0b0758affd93..46417f445bf8 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -84,8 +84,8 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { policyTagLists .map((policyTagList) => Object.values(policyTagList.tags || []) - .filter((value) => value.name === PolicyUtils.getCleanedTagName(value.name)) .sort((a, b) => localeCompare(a.name, b.name)) + .filter((value) => PolicyUtils.getCleanedTagName(value.name) === value.name) .map((value) => ({ value: value.name, text: value.name, From cb96df9e670a724043bafa8b39e2c585b1ef37e8 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 21 Mar 2024 15:39:20 +0100 Subject: [PATCH 007/206] Refactoring ... --- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index a355cc062f3d..46417f445bf8 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -85,6 +85,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { .map((policyTagList) => Object.values(policyTagList.tags || []) .sort((a, b) => localeCompare(a.name, b.name)) + .filter((value) => PolicyUtils.getCleanedTagName(value.name) === value.name) .map((value) => ({ value: value.name, text: value.name, 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 008/206] 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]} >