diff --git a/src/App.js b/src/App.js
index 3553900bbc7f..8045f4eb30ad 100644
--- a/src/App.js
+++ b/src/App.js
@@ -6,6 +6,7 @@ import Onyx from 'react-native-onyx';
import {PickerStateProvider} from 'react-native-picker-select';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import '../wdyr';
+import ActiveWorkspaceContextProvider from './components/ActiveWorkspace/ActiveWorkspaceProvider';
import ColorSchemeWrapper from './components/ColorSchemeWrapper';
import ComposeProviders from './components/ComposeProviders';
import CustomStatusBarAndBackground from './components/CustomStatusBarAndBackground';
@@ -69,6 +70,7 @@ function App() {
PickerStateProvider,
EnvironmentProvider,
CustomStatusBarAndBackgroundContextProvider,
+ ActiveWorkspaceContextProvider,
]}
>
diff --git a/src/CONST.ts b/src/CONST.ts
index 79599e54478c..e0e575303108 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -3065,7 +3065,11 @@ const CONST = {
DEFAULT: 5,
CAROUSEL: 3,
},
+ BRICK_ROAD: {
+ GBR: 'info',
+ RBR: 'error',
+ },
VIOLATIONS: {
ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired',
AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index e31a134d1b6d..75f92c76c4b7 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -57,7 +57,7 @@ const ROUTES = {
route: 'bank-account/:stepToOpen?',
getRoute: (stepToOpen = '', policyID = '', backTo?: string) => getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}`, backTo),
},
-
+ WORKSPACE_SWITCHER: 'workspaceSwitcher',
SETTINGS: 'settings',
SETTINGS_PROFILE: 'settings/profile',
SETTINGS_SHARE_CODE: 'settings/shareCode',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index da28de963058..d9e637d0f316 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -85,6 +85,10 @@ const SCREENS = {
},
LEFT_MODAL: {
SEARCH: 'Search',
+ WORKSPACE_SWITCHER: 'WorkspaceSwitcher',
+ },
+ WORKSPACE_SWITCHER: {
+ ROOT: 'WorkspaceSwitcher_Root',
},
RIGHT_MODAL: {
SETTINGS: 'Settings',
diff --git a/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx
new file mode 100644
index 000000000000..aefc4954f921
--- /dev/null
+++ b/src/components/ActiveWorkspace/ActiveWorkspaceContext.tsx
@@ -0,0 +1,11 @@
+import {createContext} from 'react';
+
+type ActiveWorkspaceContextType = {
+ activeWorkspaceID?: string;
+ setActiveWorkspaceID: (activeWorkspaceID?: string) => void;
+}
+
+const ActiveWorkspaceContext = createContext({activeWorkspaceID: undefined, setActiveWorkspaceID: () => undefined});
+
+export default ActiveWorkspaceContext;
+export {type ActiveWorkspaceContextType};
\ No newline at end of file
diff --git a/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx b/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx
new file mode 100644
index 000000000000..a602a6b60269
--- /dev/null
+++ b/src/components/ActiveWorkspace/ActiveWorkspaceProvider.tsx
@@ -0,0 +1,17 @@
+import React, {useMemo, useState} from 'react';
+import ActiveWorkspaceContext from './ActiveWorkspaceContext';
+
+function ActiveWorkspaceContextProvider({children}: React.PropsWithChildren) {
+ const [activeWorkspaceID, setActiveWorkspaceID] = useState(undefined);
+
+ const value = useMemo(
+ () => ({
+ activeWorkspaceID,
+ setActiveWorkspaceID,
+ }), [activeWorkspaceID]
+ )
+
+ return {children};
+}
+
+export default ActiveWorkspaceContextProvider;
\ No newline at end of file
diff --git a/src/components/EnvironmentBadge.tsx b/src/components/EnvironmentBadge.tsx
index 3a8445f62880..782144414688 100644
--- a/src/components/EnvironmentBadge.tsx
+++ b/src/components/EnvironmentBadge.tsx
@@ -32,6 +32,7 @@ function EnvironmentBadge() {
badgeStyles={[styles.alignSelfStart, styles.headerEnvBadge]}
textStyles={[styles.headerEnvBadgeText]}
environment={environment}
+ pressable
/>
);
}
diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx
index a6f34cd459fc..8ab26595b982 100644
--- a/src/components/MultipleAvatars.tsx
+++ b/src/components/MultipleAvatars.tsx
@@ -153,6 +153,7 @@ function MultipleAvatars({
)}
+ {option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO && (
+
+
+
+ )}
{showSelectedState && (
<>
{shouldShowSelectedStateAsButton && !isSelected ? (
diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js
index bd3695eb7aa9..0441cdf3b709 100644
--- a/src/components/OptionsList/BaseOptionsList.js
+++ b/src/components/OptionsList/BaseOptionsList.js
@@ -191,6 +191,10 @@ function BaseOptionsList({
return true;
}
+ if (option.policyID && option.policyID === item.policyID) {
+ return true;
+ }
+
if (_.isEmpty(option.name)) {
return false;
}
@@ -201,7 +205,7 @@ function BaseOptionsList({
return (
0 && shouldHaveOptionSeparator}
shouldDisableRowInnerPadding={shouldDisableRowInnerPadding}
diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js
index 792073b72613..3611922729d9 100755
--- a/src/components/OptionsSelector/BaseOptionsSelector.js
+++ b/src/components/OptionsSelector/BaseOptionsSelector.js
@@ -8,7 +8,7 @@ import Button from '@components/Button';
import FixedFooter from '@components/FixedFooter';
import FormHelpMessage from '@components/FormHelpMessage';
import Icon from '@components/Icon';
-import {Info} from '@components/Icon/Expensicons';
+import {Info, MagnifyingGlass} from '@components/Icon/Expensicons';
import OptionsList from '@components/OptionsList';
import {PressableWithoutFeedback} from '@components/Pressable';
import ShowMoreButton from '@components/ShowMoreButton';
@@ -492,6 +492,7 @@ class BaseOptionsSelector extends Component {
spellCheck={false}
shouldInterceptSwipe={this.props.shouldTextInputInterceptSwipe}
isLoading={this.props.isLoadingNewOptions}
+ iconLeft={MagnifyingGlass}
testID="options-selector-input"
/>
);
@@ -502,6 +503,7 @@ class BaseOptionsSelector extends Component {
onSelectRow={this.props.onSelectRow ? this.selectRow : undefined}
sections={this.state.sections}
focusedIndex={this.state.focusedIndex}
+ disableFocusOptions={this.props.disableFocusOptions}
selectedOptions={this.props.selectedOptions}
canSelectMultipleOptions={this.props.canSelectMultipleOptions}
shouldShowMultipleOptionSelectorAsButton={this.props.shouldShowMultipleOptionSelectorAsButton}
diff --git a/src/components/OptionsSelector/optionsSelectorPropTypes.js b/src/components/OptionsSelector/optionsSelectorPropTypes.js
index e52187fa76d7..847409c70269 100644
--- a/src/components/OptionsSelector/optionsSelectorPropTypes.js
+++ b/src/components/OptionsSelector/optionsSelectorPropTypes.js
@@ -72,6 +72,9 @@ const propTypes = {
/** Whether to disable interactivity of option rows */
isDisabled: PropTypes.bool,
+ /** Whether to disable focus options of rows */
+ disableFocusOptions: PropTypes.bool,
+
/** Display the text of the option in bold font style */
boldStyle: PropTypes.bool,
@@ -163,6 +166,7 @@ const defaultProps = {
shouldShowOptions: true,
disableArrowKeysActions: false,
isDisabled: false,
+ disableFocusOptions: false,
shouldHaveOptionSeparator: false,
initiallyFocusedOptionKey: undefined,
maxLength: CONST.SEARCH_MAX_LENGTH,
diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx
index 9c3899979aaa..02e4476975a3 100644
--- a/src/components/TextInput/BaseTextInput/index.tsx
+++ b/src/components/TextInput/BaseTextInput/index.tsx
@@ -36,6 +36,7 @@ function BaseTextInput(
placeholder = '',
errorText = '',
icon = null,
+ iconLeft = null,
textInputContainerStyles,
touchableInputWrapperStyle,
containerStyles,
@@ -317,6 +318,16 @@ function BaseTextInput(
>
) : null}
+ {!inputProps.secureTextEntry && iconLeft && (
+
+
+
+ )}
{Boolean(prefixCharacter) && (
;
diff --git a/src/components/WorkspaceSwitcherButton.tsx b/src/components/WorkspaceSwitcherButton.tsx
index 581425ad76c1..6decdfedc032 100644
--- a/src/components/WorkspaceSwitcherButton.tsx
+++ b/src/components/WorkspaceSwitcherButton.tsx
@@ -1,22 +1,46 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import useLocalize from '@hooks/useLocalize';
import CONST from '@src/CONST';
+import Navigation from '@libs/Navigation/Navigation';
+import ROUTES from '@src/ROUTES';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
+import {getPolicy, getDefaultWorkspaceAvatar} from "@libs/ReportUtils"
import * as Expensicons from './Icon/Expensicons';
import {PressableWithFeedback} from './Pressable';
import SubscriptAvatar from './SubscriptAvatar';
function WorkspaceSwitcherButton() {
const {translate} = useLocalize();
+ const {activeWorkspaceID} = useActiveWorkspace();
+
+
+ const {source, name, type} = useMemo(() => {
+
+ if(!activeWorkspaceID) {
+ return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR}
+ }
+
+ const policy = getPolicy(activeWorkspaceID);
+ const avatar = policy?.avatar && policy?.avatar?.length > 0 ? policy.avatar : getDefaultWorkspaceAvatar(policy?.name);
+ return {
+ source: avatar,
+ name: policy?.name,
+ type: CONST.ICON_TYPE_WORKSPACE,
+ }
+ }, [activeWorkspaceID]);
+
return (
{}}
+ onPress={() => {
+ Navigation.navigate(ROUTES.WORKSPACE_SWITCHER);
+ }}
>
({cardStyle: styles.navigationScreenCardStyle, headerShown: false}),
);
+const WorkspaceSwitcherModalStackNavigator = createModalStackNavigator({
+ [SCREENS.WORKSPACE_SWITCHER.ROOT]: () => require('../../../pages/WorkspaceSwitcherPage').default as React.ComponentType,
+});
const SettingsModalStackNavigator = createModalStackNavigator({
[SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../pages/ShareCodePage').default as React.ComponentType,
@@ -310,6 +314,7 @@ export {
PrivateNotesModalStackNavigator,
ProfileModalStackNavigator,
ReferralModalStackNavigator,
+ WorkspaceSwitcherModalStackNavigator,
ReimbursementAccountModalStackNavigator,
ReportDetailsModalStackNavigator,
ReportParticipantsModalStackNavigator,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx
index 4e78231b6b6e..8f76d8fbdd7b 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx
@@ -35,6 +35,10 @@ function LeftModalNavigator({navigation}: LeftModalNavigatorProps) {
name={SCREENS.LEFT_MODAL.SEARCH}
component={ModalStackNavigators.SearchModalStackNavigator}
/>
+
diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts
index 55b10ce0b9d5..78079e32b9dd 100644
--- a/src/libs/Navigation/linkingConfig.ts
+++ b/src/libs/Navigation/linkingConfig.ts
@@ -80,6 +80,13 @@ const linkingConfig: LinkingOptions = {
[SCREENS.SEARCH_ROOT]: ROUTES.SEARCH,
},
},
+ [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: {
+ screens: {
+ [SCREENS.WORKSPACE_SWITCHER.ROOT]: {
+ path: ROUTES.WORKSPACE_SWITCHER,
+ },
+ },
+ },
},
},
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index ebb9824c48c3..e7d204995162 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -79,6 +79,10 @@ type CentralPaneNavigatorParamList = {
};
};
+type WorkspaceSwitcherNavigatorParamList = {
+ [SCREENS.WORKSPACE_SWITCHER.ROOT]: undefined;
+};
+
type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.ROOT]: undefined;
[SCREENS.SETTINGS.SHARE_CODE]: undefined;
@@ -367,6 +371,7 @@ type PrivateNotesNavigatorParamList = {
type LeftModalNavigatorParamList = {
[SCREENS.LEFT_MODAL.SEARCH]: NavigatorScreenParams;
+ [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: NavigatorScreenParams;
};
type RightModalNavigatorParamList = {
@@ -514,4 +519,5 @@ export type {
ReferralDetailsNavigatorParamList,
ReimbursementAccountNavigatorParamList,
State,
+ WorkspaceSwitcherNavigatorParamList,
};
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index 2c5c72ab1c39..3e8950e393f3 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -86,10 +86,7 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, policyMemb
*/
function shouldShowPolicy(policy: OnyxEntry, isOffline: boolean): boolean {
return (
- !!policy &&
- policy?.isPolicyExpenseChatEnabled &&
- policy?.role === CONST.POLICY.ROLE.ADMIN &&
- (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0)
+ !!policy && policy?.isPolicyExpenseChatEnabled && (isOffline || policy?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || Object.keys(policy.errors ?? {}).length > 0)
);
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 0d7658adf180..a91a11362474 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -25,6 +25,7 @@ import type DeepValueOf from '@src/types/utils/DeepValueOf';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject';
import type IconAsset from '@src/types/utils/IconAsset';
+import Log from "./Log"
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import isReportMessageAttachment from './isReportMessageAttachment';
@@ -326,7 +327,7 @@ type OptionData = {
text: string;
alternateText?: string | null;
allReportErrors?: Errors | null;
- brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null;
+ brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | typeof CONST.BRICK_ROAD_INDICATOR_STATUS.INFO | '' | null;
tooltipText?: string | null;
alternateTextMaxLines?: number;
boldStyle?: boolean;
@@ -3437,11 +3438,18 @@ function shouldHideReport(report: OnyxEntry, currentReportId: string): b
* This logic is very specific and the order of the logic is very important. It should fail quickly in most cases and also
* filter out the majority of reports before filtering out very specific minority of reports.
*/
-function shouldReportBeInOptionList(report: OnyxEntry, currentReportId: string, isInGSDMode: boolean, betas: Beta[], policies: OnyxCollection, excludeEmptyChats = false) {
+function shouldReportBeInOptionList(report: OnyxEntry, currentReportId: string, isInGSDMode: boolean, betas: Beta[], policies: OnyxCollection, excludeEmptyChats = false, activeWorkspaceID: string | undefined = undefined) {
const isInDefaultMode = !isInGSDMode;
// Exclude reports that have no data because there wouldn't be anything to show in the option item.
// This can happen if data is currently loading from the server or a report is in various stages of being created.
// This can also happen for anyone accessing a public room or archived room for which they don't have access to the underlying policy.
+ // Optionally exclude reports that do not belong to currently active workspace
+
+ Log.info(`active workspace id ${activeWorkspaceID}`)
+ if (activeWorkspaceID && report?.policyID !== activeWorkspaceID && !isConciergeChatReport(report)) {
+ return false;
+ }
+
if (
!report?.reportID ||
!report?.type ||
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 6e46ec320066..b47c66e1a4d8 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -120,6 +120,7 @@ function getOrderedReportIDs(
policies: Record,
priorityMode: ValueOf,
allReportActions: OnyxCollection,
+ activeWorkspaceID: string | undefined = undefined,
): string[] {
// Generate a unique cache key based on the function arguments
const cachedReportsKey = JSON.stringify(
@@ -151,7 +152,7 @@ function getOrderedReportIDs(
const isInDefaultMode = !isInGSDMode;
const allReportsDictValues = Object.values(allReports);
// Filter out all the reports that shouldn't be displayed
- const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, true));
+ const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, true, activeWorkspaceID));
if (reportsToDisplay.length === 0) {
// Display Concierge chat report when there is no report to be displayed
diff --git a/src/libs/WorkspacesUtils.ts b/src/libs/WorkspacesUtils.ts
new file mode 100644
index 000000000000..5ce3f1f465fd
--- /dev/null
+++ b/src/libs/WorkspacesUtils.ts
@@ -0,0 +1,102 @@
+import Onyx, {OnyxCollection} from 'react-native-onyx';
+import {ValueOf} from 'type-fest';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {Report} from '@src/types/onyx';
+import * as OptionsListUtils from './OptionsListUtils';
+import * as ReportActionsUtils from './ReportActionsUtils';
+import * as ReportUtils from './ReportUtils';
+
+let allReports: OnyxCollection;
+
+type BrickRoad = ValueOf | undefined;
+
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (value) => (allReports = value),
+});
+
+/**
+ * @param report
+ * @returns BrickRoad for the policy passed as a param
+ */
+const getBrickRoadForPolicy = (report: Report): BrickRoad => {
+ const reportActions = ReportActionsUtils.getAllReportActions(report.reportID);
+ const reportErrors = OptionsListUtils.getAllReportErrors(report, reportActions);
+ const doesReportContainErrors = Object.keys(reportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined;
+ if (doesReportContainErrors) {
+ return CONST.BRICK_ROAD.RBR;
+ }
+
+ // To determine if the report requires attention from the current user, we need to load the parent report action
+ let itemParentReportAction = {};
+ if (report.parentReportID) {
+ const itemParentReportActions = ReportActionsUtils.getAllReportActions(report.parentReportID);
+ itemParentReportAction = report.parentReportActionID ? itemParentReportActions[report.parentReportActionID] : {};
+ }
+ const reportOption = {...report, isUnread: ReportUtils.isUnread(report), isUnreadWithMention: ReportUtils.isUnreadWithMention(report)};
+ const shouldShowGreenDotIndicator = ReportUtils.requiresAttentionFromCurrentUser(reportOption, itemParentReportAction);
+ return shouldShowGreenDotIndicator ? CONST.BRICK_ROAD.GBR : undefined;
+};
+
+/**
+ * @returns a map where the keys are policyIDs and the values are BrickRoads for each policy
+ */
+function getWorkspacesBrickRoads(): Record {
+ if (!allReports) {
+ return {};
+ }
+
+ // The key in this map is the workspace id
+ const workspacesBrickRoadsMap: Record = {};
+
+ Object.keys(allReports).forEach((report) => {
+ const policyID = allReports?.[report]?.policyID;
+ const policyReport = allReports ? allReports[report] : null;
+ if (!policyID || !policyReport || workspacesBrickRoadsMap[policyID] === CONST.BRICK_ROAD.RBR) {
+ return;
+ }
+ const workspaceBrickRoad = getBrickRoadForPolicy(policyReport);
+
+ if (!workspaceBrickRoad && !!workspacesBrickRoadsMap[policyID]) {
+ return;
+ }
+
+ workspacesBrickRoadsMap[policyID] = workspaceBrickRoad;
+ });
+
+ return workspacesBrickRoadsMap;
+}
+
+/**
+ * @returns a map where the keys are policyIDs and the values are truthy booleans if policy has unread content
+ */
+function getWorkspacesUnreadStatuses(): Record {
+ if (!allReports) {
+ return {};
+ }
+
+ const workspacesUnreadStatuses: Record = {};
+
+ Object.keys(allReports).forEach((report) => {
+ const policyID = allReports?.[report]?.policyID;
+ const policyReport = allReports ? allReports[report] : null;
+ if (!policyID || !policyReport) {
+ return;
+ }
+
+ const unreadStatus = ReportUtils.isUnread(policyReport);
+
+ if (unreadStatus) {
+ workspacesUnreadStatuses[policyID] = true;
+ } else {
+ workspacesUnreadStatuses[policyID] = false;
+ }
+ });
+
+ return workspacesUnreadStatuses;
+}
+
+export {getBrickRoadForPolicy, getWorkspacesBrickRoads, getWorkspacesUnreadStatuses};
+export type {BrickRoad};
diff --git a/src/pages/WorkspaceSwitcherPage.js b/src/pages/WorkspaceSwitcherPage.js
new file mode 100644
index 000000000000..3fc46acc1994
--- /dev/null
+++ b/src/pages/WorkspaceSwitcherPage.js
@@ -0,0 +1,317 @@
+/* eslint-disable react-hooks/exhaustive-deps */
+import PropTypes from 'prop-types';
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import _ from 'underscore';
+import HeaderPageLayout from '@components/HeaderPageLayout';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import OptionRow from '@components/OptionRow';
+import OptionsSelector from '@components/OptionsSelector';
+import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import Text from '@components/Text';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useTheme from '@hooks/useTheme';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import * as PolicyUtils from '@libs/PolicyUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import {getWorkspacesBrickRoads, getWorkspacesUnreadStatuses} from '@libs/WorkspacesUtils';
+import * as App from '@userActions/App';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
+import WorkspaceCardCreateAWorkspace from './workspace/card/WorkspaceCardCreateAWorkspace';
+
+const propTypes = {
+ /** The list of this user's policies */
+ policies: PropTypes.objectOf(
+ PropTypes.shape({
+ /** The ID of the policy */
+ id: PropTypes.string,
+
+ /** The name of the policy */
+ name: PropTypes.string,
+
+ /** The type of the policy */
+ type: PropTypes.string,
+
+ /** The user's role in the policy */
+ role: PropTypes.string,
+
+ /** The current action that is waiting to happen on the policy */
+ pendingAction: PropTypes.oneOf(_.values(CONST.RED_BRICK_ROAD_PENDING_ACTION)),
+ }),
+ ),
+};
+
+const defaultProps = {
+ policies: {},
+};
+
+const MINIMUM_WORKSPACES_TO_SHOW_SEARCH = 8;
+const EXPENSIFY_TITLE = 'Expensify';
+
+function WorkspaceSwitcherPage({policies}) {
+ 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) => {
+ if (policyId && policyId !== activeWorkspaceID) {
+ return brickRoadsForPolicies[policyId];
+ }
+
+ if (_.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD.RBR)) {
+ return CONST.BRICK_ROAD.RBR;
+ }
+
+ if (_.values(brickRoadsForPolicies).includes(CONST.BRICK_ROAD.GBR)) {
+ return CONST.BRICK_ROAD.GBR;
+ }
+
+ return undefined;
+ },
+ [activeWorkspaceID, brickRoadsForPolicies],
+ );
+
+ const hasUnreadData = useCallback(
+ // TO DO: Implement checking if policy has some unread data
+ // eslint-disable-next-line no-unused-vars
+ (policyId) => {
+ if (policyId) {
+ return unreadStatusesForPolicies[policyId];
+ }
+
+ return _.some(_.values(unreadStatusesForPolicies), (status) => status);
+ },
+ [unreadStatusesForPolicies],
+ );
+
+ const selectPolicy = useCallback((option) => {
+ const policyID = option.policyID;
+
+ if (policyID) {
+ setSelectedOption(option);
+ } else {
+ setSelectedOption(undefined);
+ }
+ // Temporary: This will be handled in custom navigation function that also puts policyID in BottomTabNavigator state
+ setActiveWorkspaceID(policyID);
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.HOME);
+ }, []);
+
+ const onChangeText = useCallback((newSearchTerm) => {
+ setSearchTerm(newSearchTerm);
+ }, []);
+
+ const usersWorkspaces = useMemo(
+ () =>
+ _.chain(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,
+ }))
+ .sortBy((policy) => policy.text.toLowerCase())
+ .value(),
+ [policies, getIndicatorTypeForPolicy, hasUnreadData],
+ );
+
+ const filteredUserWorkspaces = useMemo(() => _.filter(usersWorkspaces, (policy) => policy.text.toLowerCase().startsWith(searchTerm.toLowerCase())), [searchTerm, usersWorkspaces]);
+
+ const usersWorkspacesSectionData = useMemo(
+ () => ({
+ data: filteredUserWorkspaces,
+ shouldShow: true,
+ indexOffset: 0,
+ }),
+ [filteredUserWorkspaces],
+ );
+
+ const everythingSection = useMemo(() => {
+ const option = {
+ text: EXPENSIFY_TITLE,
+ icons: [
+ {
+ source: Expensicons.ExpensifyAppIcon,
+ name: EXPENSIFY_TITLE,
+ type: CONST.ICON_TYPE_AVATAR,
+ },
+ ],
+ brickRoadIndicator: getIndicatorTypeForPolicy(undefined),
+ boldStyle: hasUnreadData(undefined),
+ };
+
+ return (
+ <>
+
+
+ {translate('workspace.switcher.everythingSection')}
+
+
+
+
+
+ >
+ );
+ }, [
+ activeWorkspaceID,
+ getIndicatorTypeForPolicy,
+ hasUnreadData,
+ selectPolicy,
+ styles.alignItemsCenter,
+ styles.flexRow,
+ styles.justifyContentBetween,
+ styles.label,
+ styles.mb3,
+ styles.mh4,
+ theme.textSupporting,
+ translate,
+ ]);
+
+ const workspacesSection = useMemo(
+ () => (
+ <>
+ 0 ? [styles.mb1] : [styles.mb3])]}>
+
+
+ {translate('common.workspaces')}
+
+
+ {
+ App.createWorkspaceWithPolicyDraftAndNavigateToIt();
+ }}
+ >
+ {({hovered}) => (
+
+ )}
+
+
+
+ {usersWorkspaces.length > 0 ? (
+ = MINIMUM_WORKSPACES_TO_SHOW_SEARCH}
+ onChangeText={onChangeText}
+ selectedOptions={selectedOption ? [selectedOption] : []}
+ onSelectRow={selectPolicy}
+ shouldPreventDefaultFocusOnSelectRow
+ highlightSelectedOptions
+ shouldShowOptions
+ autoFocus={false}
+ disableFocusOptions
+ canSelectMultipleOptions={false}
+ shouldShowSubscript={false}
+ showTitleTooltip={false}
+ contentContainerStyles={[styles.pt0, styles.mt0]}
+ />
+ ) : (
+
+ )}
+ >
+ ),
+ [
+ inputCallbackRef,
+ onChangeText,
+ searchTerm,
+ selectPolicy,
+ selectedOption,
+ styles.alignItemsEnd,
+ styles.borderRadiusNormal,
+ styles.buttonDefaultBG,
+ styles.buttonHoveredBG,
+ styles.flexRow,
+ styles.justifyContentBetween,
+ styles.label,
+ styles.mb1,
+ styles.mb3,
+ styles.mh4,
+ styles.mt0,
+ styles.mt2,
+ styles.mt3,
+ styles.p2,
+ styles.pt0,
+ theme.textSupporting,
+ translate,
+ usersWorkspaces.length,
+ usersWorkspacesSectionData,
+ ],
+ );
+
+ useEffect(() => {
+ if (!activeWorkspaceID) {
+ return;
+ }
+ const optionToSet = _.find(usersWorkspaces, (option) => option.policyID === activeWorkspaceID);
+ setSelectedOption(optionToSet);
+ }, [activeWorkspaceID, usersWorkspaces]);
+
+ return (
+
+ {everythingSection}
+ {workspacesSection}
+
+ );
+}
+
+WorkspaceSwitcherPage.propTypes = propTypes;
+WorkspaceSwitcherPage.defaultProps = defaultProps;
+WorkspaceSwitcherPage.displayName = 'WorkspaceSwitcherPage';
+
+export default withOnyx({
+ policies: {
+ key: ONYXKEYS.COLLECTION.POLICY,
+ },
+})(WorkspaceSwitcherPage);
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 17abb3bdb43b..127c8b72f336 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -20,6 +20,7 @@ import * as App from '@userActions/App';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
const basePropTypes = {
/** Toggles the navigation menu open and closed */
@@ -48,6 +49,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
const modal = useRef({});
const {translate, updateLocale} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
+ const {activeWorkspaceID} = useActiveWorkspace();
useEffect(() => {
if (!isSmallScreenWidth) {
diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js
index 580cc7909fd1..195a0eeccb04 100644
--- a/src/pages/home/sidebar/SidebarLinksData.js
+++ b/src/pages/home/sidebar/SidebarLinksData.js
@@ -16,6 +16,7 @@ import SidebarUtils from '@libs/SidebarUtils';
import reportPropTypes from '@pages/reportPropTypes';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import useActiveWorkspace from '@hooks/useActiveWorkspace';
import SidebarLinks, {basePropTypes} from './SidebarLinks';
const propTypes = {
@@ -68,12 +69,13 @@ const defaultProps = {
function SidebarLinksData({isFocused, allReportActions, betas, chatReports, currentReportID, insets, isLoadingApp, onLinkClick, policies, priorityMode, network}) {
const styles = useThemeStyles();
+ const {activeWorkspaceID} = useActiveWorkspace();
const {translate} = useLocalize();
const reportIDsRef = useRef(null);
const isLoading = isLoadingApp;
const optionListItems = useMemo(() => {
- const reportIDs = SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions);
+ const reportIDs = SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions, activeWorkspaceID);
if (deepEqual(reportIDsRef.current, reportIDs)) {
return reportIDsRef.current;
@@ -85,7 +87,7 @@ function SidebarLinksData({isFocused, allReportActions, betas, chatReports, curr
reportIDsRef.current = reportIDs;
}
return reportIDsRef.current || [];
- }, [allReportActions, betas, chatReports, policies, priorityMode, isLoading, network.isOffline]);
+ }, [chatReports, betas, policies, priorityMode, allReportActions, activeWorkspaceID, isLoading, network.isOffline]);
// We need to make sure the current report is in the list of reports, but we do not want
// to have to re-generate the list every time the currentReportID changes. To do that
@@ -94,10 +96,10 @@ function SidebarLinksData({isFocused, allReportActions, betas, chatReports, curr
// case we re-generate the list a 2nd time with the current report included.
const optionListItemsWithCurrentReport = useMemo(() => {
if (currentReportID && !_.contains(optionListItems, currentReportID)) {
- return SidebarUtils.getOrderedReportIDs(currentReportID, chatReports, betas, policies, priorityMode, allReportActions);
+ return SidebarUtils.getOrderedReportIDs(currentReportID, chatReports, betas, policies, priorityMode, allReportActions, activeWorkspaceID);
}
return optionListItems;
- }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions]);
+ }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions, activeWorkspaceID]);
const currentReportIDRef = useRef(currentReportID);
currentReportIDRef.current = currentReportID;
diff --git a/src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx b/src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx
index 9e45bd143e7e..3546a437b2e2 100644
--- a/src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx
+++ b/src/pages/workspace/card/WorkspaceCardCreateAWorkspace.tsx
@@ -4,6 +4,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
import Section, {CARD_LAYOUT} from '@components/Section';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import * as App from '@userActions/App';
function WorkspaceCardCreateAWorkspace() {
const styles = useThemeStyles();
@@ -19,6 +20,9 @@ function WorkspaceCardCreateAWorkspace() {
containerStyles={[styles.highlightBG]}
>