Skip to content

Commit 7aed308

Browse files
Merge pull request Expensify#41665 from software-mansion-labs/@kosmydel/not-found-view-v2
Fix: three not found view v2
2 parents 91a5c69 + e60668b commit 7aed308

9 files changed

+100
-81
lines changed

src/CONST.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2380,6 +2380,7 @@ const CONST = {
23802380
WORKSPACE_WORKFLOWS: 'WorkspaceWorkflows',
23812381
WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount',
23822382
WORKSPACE_SETTINGS: 'WorkspaceSettings',
2383+
WORKSPACE_FEATURES: 'WorkspaceFeatures',
23832384
},
23842385
get EXPENSIFY_EMAILS() {
23852386
return [

src/libs/Navigation/linkTo/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export default function linkTo(navigation: NavigationContainerRef<RootStackParam
141141
}
142142
}
143143
// All actions related to FullScreenNavigator on wide screen are pushed when comparing differences between rootState and adaptedState.
144-
if (action.payload.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR && !isNarrowLayout) {
144+
if (action.payload.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR) {
145145
return;
146146
}
147147
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;

src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import getMatchingBottomTabRouteForState from './getMatchingBottomTabRouteForSta
2121
import getMatchingCentralPaneRouteForState from './getMatchingCentralPaneRouteForState';
2222
import replacePathInNestedState from './replacePathInNestedState';
2323

24-
const RHP_SCREENS_OPENED_FROM_LHN = [SCREENS.SETTINGS.SHARE_CODE, SCREENS.SETTINGS.PROFILE.STATUS] as const;
24+
const RHP_SCREENS_OPENED_FROM_LHN = [SCREENS.SETTINGS.SHARE_CODE, SCREENS.SETTINGS.PROFILE.STATUS, SCREENS.SETTINGS.PREFERENCES.PRIORITY_MODE] satisfies Screen[];
2525

2626
type RHPScreenOpenedFromLHN = TupleToUnion<typeof RHP_SCREENS_OPENED_FROM_LHN>;
2727

@@ -174,11 +174,6 @@ function getAdaptedState(state: PartialState<NavigationState<RootStackParamList>
174174
const attachmentsScreen = state.routes.find((route) => route.name === SCREENS.ATTACHMENTS);
175175
const featureTrainingModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR);
176176

177-
if (isNarrowLayout) {
178-
metainfo.isFullScreenNavigatorMandatory = false;
179-
metainfo.isCentralPaneAndBottomTabMandatory = false;
180-
}
181-
182177
if (rhpNavigator) {
183178
// Routes
184179
// - matching bottom tab

src/pages/workspace/AccessOrNotFoundWrapper.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN
9090
return shouldShowFullScreenFallback ? (
9191
<FullPageNotFoundView
9292
shouldShow
93-
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
93+
onBackButtonPress={() => Navigation.dismissModal()}
9494
shouldForceFullScreen
9595
// eslint-disable-next-line react/jsx-props-no-spreading
9696
{...fullPageNotFoundViewProps}
@@ -155,7 +155,7 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps
155155
return (
156156
<PageNotFoundFallback
157157
policyID={policyID}
158-
shouldShowFullScreenFallback={!isFeatureEnabled}
158+
shouldShowFullScreenFallback={!isFeatureEnabled || isPolicyNotAccessible}
159159
fullPageNotFoundViewProps={fullPageNotFoundViewProps}
160160
/>
161161
);

src/pages/workspace/WorkspaceInitialPage.tsx

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {useFocusEffect, useNavigationState} from '@react-navigation/native';
22
import type {StackScreenProps} from '@react-navigation/stack';
3-
import React, {useCallback, useEffect, useMemo, useState} from 'react';
3+
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
44
import {View} from 'react-native';
55
import type {OnyxEntry} from 'react-native-onyx';
66
import {withOnyx} from 'react-native-onyx';
@@ -97,6 +97,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc
9797
const activeRoute = useNavigationState(getTopmostRouteName);
9898
const {translate} = useLocalize();
9999
const {isOffline} = useNetwork();
100+
const wasRendered = useRef(false);
100101

101102
const prevPendingFields = usePrevious(policy?.pendingFields);
102103
const policyFeatureStates = useMemo(
@@ -347,6 +348,24 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc
347348
PolicyUtils.goBackFromInvalidPolicy();
348349
}, [policy, prevPolicy]);
349350

351+
// We are checking if the user can access the route.
352+
// If user can't access the route, we are dismissing any modals that are open when the NotFound view is shown
353+
const canAccessRoute = activeRoute && menuItems.some((item) => item.routeName === activeRoute);
354+
355+
useEffect(() => {
356+
if (!shouldShowNotFoundPage && canAccessRoute) {
357+
return;
358+
}
359+
if (wasRendered.current) {
360+
return;
361+
}
362+
wasRendered.current = true;
363+
// We are dismissing any modals that are open when the NotFound view is shown
364+
Navigation.isNavigationReady().then(() => {
365+
Navigation.closeRHPFlow();
366+
});
367+
}, [canAccessRoute, shouldShowNotFoundPage]);
368+
350369
const policyAvatar = useMemo(() => {
351370
if (!policy) {
352371
return {source: Expensicons.ExpensifyAppIcon, name: CONST.WORKSPACE_SWITCHER.NAME, type: CONST.ICON_TYPE_AVATAR};

src/pages/workspace/WorkspaceInviteMessagePage.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ function WorkspaceInviteMessagePage({
9494
setWelcomeNote(getDefaultWelcomeNote());
9595
return;
9696
}
97+
if (isEmptyObject(policy)) {
98+
return;
99+
}
97100
Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID), true);
98101
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
99102
}, []);

src/pages/workspace/WorkspaceMembersPage.tsx

+57-68
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@ import {InteractionManager, View} from 'react-native';
77
import type {OnyxEntry} from 'react-native-onyx';
88
import {withOnyx} from 'react-native-onyx';
99
import Badge from '@components/Badge';
10-
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
1110
import Button from '@components/Button';
1211
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
1312
import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types';
1413
import ConfirmModal from '@components/ConfirmModal';
15-
import HeaderWithBackButton from '@components/HeaderWithBackButton';
1614
import * as Expensicons from '@components/Icon/Expensicons';
1715
import * as Illustrations from '@components/Icon/Illustrations';
1816
import MessagesRow from '@components/MessagesRow';
19-
import ScreenWrapper from '@components/ScreenWrapper';
2017
import SelectionList from '@components/SelectionList';
2118
import TableListItem from '@components/SelectionList/TableListItem';
2219
import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
@@ -48,6 +45,7 @@ import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon';
4845
import {isEmptyObject} from '@src/types/utils/EmptyObject';
4946
import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading';
5047
import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading';
48+
import WorkspacePageWithSections from './WorkspacePageWithSections';
5149

5250
type WorkspaceMembersPageOnyxProps = {
5351
/** Session info for the currently logged in user. */
@@ -71,7 +69,7 @@ function invertObject(object: Record<string, string>): Record<string, string> {
7169

7270
type MemberOption = Omit<ListItem, 'accountID'> & {accountID: number};
7371

74-
function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails, isLoadingReportData = true}: WorkspaceMembersPageProps) {
72+
function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, route, policy, session, currentUserPersonalDetails}: WorkspaceMembersPageProps) {
7573
const policyMemberEmailsToAccountIDs = useMemo(() => PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, true), [policy?.employeeList]);
7674
const styles = useThemeStyles();
7775
const StyleUtils = useStyleUtils();
@@ -526,74 +524,65 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft,
526524
};
527525

528526
return (
529-
<ScreenWrapper
530-
includeSafeAreaPaddingBottom={false}
531-
style={[styles.defaultModalContainer]}
527+
<WorkspacePageWithSections
528+
headerText={translate('workspace.common.members')}
529+
route={route}
530+
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
531+
headerContent={!isSmallScreenWidth && getHeaderButtons()}
532+
icon={Illustrations.ReceiptWrangler}
532533
testID={WorkspaceMembersPage.displayName}
534+
shouldShowLoading={false}
533535
shouldShowOfflineIndicatorInWideScreen
536+
shouldShowNonAdmin
534537
>
535-
<FullPageNotFoundView
536-
shouldShow={(isEmptyObject(policy) && !isLoadingReportData) || PolicyUtils.isPendingDeletePolicy(policy)}
537-
subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'}
538-
onBackButtonPress={PolicyUtils.goBackFromInvalidPolicy}
539-
onLinkPress={PolicyUtils.goBackFromInvalidPolicy}
540-
>
541-
<HeaderWithBackButton
542-
title={translate('workspace.common.members')}
543-
icon={Illustrations.ReceiptWrangler}
544-
onBackButtonPress={() => {
545-
Navigation.goBack();
546-
}}
547-
shouldShowBackButton={isSmallScreenWidth}
548-
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_MEMBERS}
549-
>
550-
{!isSmallScreenWidth && getHeaderButtons()}
551-
</HeaderWithBackButton>
552-
{isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>}
553-
<ConfirmModal
554-
danger
555-
title={translate('workspace.people.removeMembersTitle')}
556-
isVisible={removeMembersConfirmModalVisible}
557-
onConfirm={removeUsers}
558-
onCancel={() => setRemoveMembersConfirmModalVisible(false)}
559-
prompt={confirmModalPrompt}
560-
confirmText={translate('common.remove')}
561-
cancelText={translate('common.cancel')}
562-
onModalHide={() => {
563-
InteractionManager.runAfterInteractions(() => {
564-
if (!textInputRef.current) {
565-
return;
566-
}
567-
textInputRef.current.focus();
568-
});
569-
}}
570-
/>
571-
<View style={[styles.w100, styles.flex1]}>
572-
<SelectionList
573-
ref={selectionListRef}
574-
canSelectMultiple={isPolicyAdmin}
575-
sections={[{data, isDisabled: false}]}
576-
ListItem={TableListItem}
577-
shouldUseUserSkeletonView
578-
disableKeyboardShortcuts={removeMembersConfirmModalVisible}
579-
headerMessage={getHeaderMessage()}
580-
headerContent={!isSmallScreenWidth && getHeaderContent()}
581-
onSelectRow={openMemberDetails}
582-
shouldDebounceRowSelect={!isPolicyAdmin}
583-
onCheckboxPress={(item) => toggleUser(item.accountID)}
584-
onSelectAll={() => toggleAllUsers(data)}
585-
onDismissError={dismissError}
586-
showLoadingPlaceholder={isLoading}
587-
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
588-
textInputRef={textInputRef}
589-
customListHeader={getCustomListHeader()}
590-
listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
591-
listHeaderContent={isSmallScreenWidth ? <View style={[styles.pl5, styles.pr5]}>{getHeaderContent()}</View> : null}
592-
showScrollIndicator={false}
538+
{() => (
539+
<>
540+
{isSmallScreenWidth && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>}
541+
<ConfirmModal
542+
danger
543+
title={translate('workspace.people.removeMembersTitle')}
544+
isVisible={removeMembersConfirmModalVisible}
545+
onConfirm={removeUsers}
546+
onCancel={() => setRemoveMembersConfirmModalVisible(false)}
547+
prompt={confirmModalPrompt}
548+
confirmText={translate('common.remove')}
549+
cancelText={translate('common.cancel')}
550+
onModalHide={() => {
551+
InteractionManager.runAfterInteractions(() => {
552+
if (!textInputRef.current) {
553+
return;
554+
}
555+
textInputRef.current.focus();
556+
});
557+
}}
593558
/>
594-
</View>
595-
</FullPageNotFoundView>
596-
</ScreenWrapper>
559+
<View style={[styles.w100, styles.flex1]}>
560+
<SelectionList
561+
ref={selectionListRef}
562+
canSelectMultiple={isPolicyAdmin}
563+
sections={[{data, isDisabled: false}]}
564+
ListItem={TableListItem}
565+
shouldUseUserSkeletonView
566+
disableKeyboardShortcuts={removeMembersConfirmModalVisible}
567+
headerMessage={getHeaderMessage()}
568+
headerContent={!isSmallScreenWidth && getHeaderContent()}
569+
onSelectRow={openMemberDetails}
570+
shouldDebounceRowSelect={!isPolicyAdmin}
571+
onCheckboxPress={(item) => toggleUser(item.accountID)}
572+
onSelectAll={() => toggleAllUsers(data)}
573+
onDismissError={dismissError}
574+
showLoadingPlaceholder={isLoading}
575+
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
576+
textInputRef={textInputRef}
577+
customListHeader={getCustomListHeader()}
578+
listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]}
579+
listHeaderContent={isSmallScreenWidth ? <View style={[styles.pl5, styles.pr5]}>{getHeaderContent()}</View> : null}
580+
showScrollIndicator={false}
581+
/>
582+
</View>
583+
</>
584+
)}
585+
</WorkspacePageWithSections>
597586
);
598587
}
599588

src/pages/workspace/WorkspacePageWithSections.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
8282
* */
8383
icon?: IconAsset;
8484

85+
/** Content to be added to the header */
86+
headerContent?: ReactNode;
87+
88+
/** TestID of the component */
89+
testID?: string;
90+
8591
/** Whether the page is loading, example any other API call in progres */
8692
isLoading?: boolean;
8793
};
@@ -112,6 +118,8 @@ function WorkspacePageWithSections({
112118
shouldShowLoading = true,
113119
shouldShowOfflineIndicatorInWideScreen = false,
114120
shouldShowNonAdmin = false,
121+
headerContent,
122+
testID,
115123
shouldShowNotFoundPage = false,
116124
isLoading: isPageLoading = false,
117125
}: WorkspacePageWithSectionsProps) {
@@ -160,7 +168,7 @@ function WorkspacePageWithSections({
160168
includeSafeAreaPaddingBottom={false}
161169
shouldEnablePickerAvoiding={false}
162170
shouldEnableMaxHeight
163-
testID={WorkspacePageWithSections.displayName}
171+
testID={testID ?? WorkspacePageWithSections.displayName}
164172
shouldShowOfflineIndicatorInWideScreen={shouldShowOfflineIndicatorInWideScreen && !shouldShow}
165173
>
166174
<FullPageNotFoundView
@@ -177,7 +185,9 @@ function WorkspacePageWithSections({
177185
onBackButtonPress={() => Navigation.goBack(backButtonRoute)}
178186
icon={icon ?? undefined}
179187
style={styles.headerBarDesktopHeight}
180-
/>
188+
>
189+
{headerContent}
190+
</HeaderWithBackButton>
181191
{(isLoading || firstRender.current) && shouldShowLoading && isFocused ? (
182192
<FullScreenLoadingIndicator style={[styles.flex1, styles.pRelative]} />
183193
) : (

src/pages/workspace/WorkspaceProfilePage.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,13 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {
143143
headerText={translate('workspace.common.profile')}
144144
route={route}
145145
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_PROFILE}
146-
shouldShowLoading={false}
146+
// When we create a new workspaces, the policy prop will not be set on the first render. Therefore, we have to delay rendering until it has been set in Onyx.
147+
shouldShowLoading={policy === undefined}
147148
shouldUseScrollView
148149
shouldShowOfflineIndicatorInWideScreen
149150
shouldShowNonAdmin
150151
icon={Illustrations.House}
152+
shouldShowNotFoundPage={policy === undefined}
151153
>
152154
{(hasVBA?: boolean) => (
153155
<View style={[styles.flex1, styles.mt3, isSmallScreenWidth ? styles.workspaceSectionMobile : styles.workspaceSection]}>

0 commit comments

Comments
 (0)