Skip to content

Commit 5a83e12

Browse files
committed
restore policy/domain validation via collection subscription
1 parent c488834 commit 5a83e12

3 files changed

Lines changed: 102 additions & 38 deletions

File tree

src/hooks/useRestoreWorkspacesTabOnNavigate.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import {getTabState} from '@libs/Navigation/helpers/tabNavigatorUtils';
77
import navigationRef from '@libs/Navigation/navigationRef';
88
import type {DomainSplitNavigatorParamList, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
99
import NAVIGATORS from '@src/NAVIGATORS';
10+
import ONYXKEYS from '@src/ONYXKEYS';
1011
import type SCREENS from '@src/SCREENS';
12+
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
13+
import useOnyx from './useOnyx';
1114
import useResponsiveLayout from './useResponsiveLayout';
1215

1316
type WorkspaceRouteType = NavigationState['routes'][number] | NonNullable<PartialState<NavigationState>['routes']>[number];
@@ -59,12 +62,17 @@ function getWorkspacesTabState(route: WorkspaceRouteType | undefined): Navigatio
5962
* or a specific domain page. When the user navigates away and comes back to the tab,
6063
* this hook ensures they return to whichever of those they had open last — not always the list.
6164
*
62-
* Resolves nav state and route IDs at click time inside the returned callback so the hook
63-
* has no reactive subscriptions to nav state — unrelated navigations (e.g. opening a report)
64-
* don't trigger re-renders. The destination workspace/domain page handles invalid IDs.
65+
* Resolves nav state and route IDs at click time inside the returned callback so the hook has
66+
* no reactive subscriptions to nav state — unrelated navigations (e.g. opening a report) don't
67+
* trigger re-renders. The POLICY/DOMAIN collections are subscribed at hook level so the click
68+
* handler can validate the last viewed workspace/domain via closure; these collections rarely
69+
* tick during navigation.
6570
*/
6671
function useRestoreWorkspacesTabOnNavigate() {
6772
const {shouldUseNarrowLayout} = useResponsiveLayout();
73+
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
74+
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
75+
const [allDomains] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN);
6876

6977
return () => {
7078
const rootState = navigationRef.isReady() ? navigationRef.getRootState() : undefined;
@@ -73,13 +81,21 @@ function useRestoreWorkspacesTabOnNavigate() {
7381
const workspacesTabState = getWorkspacesTabState(lastWorkspacesTabNavigatorRoute);
7482

7583
const params = workspacesTabState?.routes?.at(0)?.params as WorkspaceParams | undefined;
76-
const policyID = params && 'policyID' in params ? params.policyID : undefined;
77-
const domainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined;
84+
const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined;
85+
const paramsDomainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined;
86+
87+
const policy =
88+
lastWorkspacesTabNavigatorRoute?.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR && paramsPolicyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${paramsPolicyID}`] : undefined;
89+
const domain =
90+
lastWorkspacesTabNavigatorRoute?.name === NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR && paramsDomainAccountID
91+
? allDomains?.[`${ONYXKEYS.COLLECTION.DOMAIN}${paramsDomainAccountID}`]
92+
: undefined;
7893

7994
navigateToWorkspacesPage({
8095
shouldUseNarrowLayout,
81-
policyID,
82-
domainAccountID,
96+
currentUserLogin,
97+
policy,
98+
domain,
8399
lastWorkspacesTabNavigatorRoute,
84100
topmostFullScreenRoute,
85101
workspacesTabState,

src/libs/Navigation/helpers/navigateToWorkspacesPage.ts

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser';
44
import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState';
55
import Navigation from '@libs/Navigation/Navigation';
66
import navigationRef from '@libs/Navigation/navigationRef';
7+
import {isPendingDeletePolicy, shouldShowPolicy as shouldShowPolicyUtil} from '@libs/PolicyUtils';
78
import NAVIGATORS from '@src/NAVIGATORS';
89
import ROUTES from '@src/ROUTES';
910
import type {Route} from '@src/ROUTES';
1011
import SCREENS from '@src/SCREENS';
12+
import type {Domain, Policy} from '@src/types/onyx';
1113
import getActiveTabName from './getActiveTabName';
1214
import getPathFromState from './getPathFromState';
1315
import {getTabState} from './tabNavigatorUtils';
@@ -24,9 +26,10 @@ function wrapStateInNavigators(state: PartialState<NavigationState>, navigators:
2426
}
2527

2628
type Params = {
29+
currentUserLogin?: string;
2730
shouldUseNarrowLayout: boolean;
28-
policyID?: string;
29-
domainAccountID?: number;
31+
policy?: Policy;
32+
domain?: Domain;
3033
lastWorkspacesTabNavigatorRoute?: RouteType;
3134
topmostFullScreenRoute?: RouteType;
3235
/**
@@ -40,7 +43,7 @@ type Params = {
4043
workspacesTabState?: NavigationState | PartialState<NavigationState>;
4144
};
4245

43-
const navigateToWorkspacesPage = ({shouldUseNarrowLayout, policyID, domainAccountID, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute, workspacesTabState}: Params) => {
46+
const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, policy, domain, lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute, workspacesTabState}: Params) => {
4447
const rootState = navigationRef.getRootState();
4548
const focusedRoute = rootState ? findFocusedRoute(rootState) : undefined;
4649
const isOnWorkspacesList = focusedRoute?.name === SCREENS.WORKSPACES_LIST;
@@ -90,40 +93,45 @@ const navigateToWorkspacesPage = ({shouldUseNarrowLayout, policyID, domainAccoun
9093
return;
9194
}
9295

93-
// Workspace route found: navigate to last workspace screen by ID.
94-
// Validation is deferred to the destination — if the policy was deleted or access
95-
// revoked, the workspace page will redirect to the list itself.
96+
// Workspace route found: try to restore last workspace screen.
9697
if (lastWorkspacesTabNavigatorRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR) {
97-
if (!policyID) {
98+
const shouldShowPolicy = shouldShowPolicyUtil(policy, false, currentUserLogin);
99+
const isPendingDelete = isPendingDeletePolicy(policy);
100+
101+
// Workspace is not accessible or is being deleted: go to list.
102+
if (!shouldShowPolicy || isPendingDelete) {
98103
Navigation.navigate(ROUTES.WORKSPACES_LIST.route);
99104
return;
100105
}
101-
// Synthesize a URL from the captured WorkspaceSplitNavigator inner state and navigate
102-
// to it. URL-based navigation goes through `getStateFromPath`, which produces a fully
103-
// formed nested state and reliably handles pushing a fresh TabNavigator on top of an
104-
// existing fullscreen stack. The state has to be wrapped with its full ancestor chain
105-
// (TAB_NAVIGATOR > WORKSPACE_NAVIGATOR > WORKSPACE_SPLIT_NAVIGATOR) so `getPathFromState`
106-
// can match the linking-config hierarchy and produce a real URL like
107-
// `/workspaces/POLICY_ID/workflows`; otherwise the resolver falls back to navigator
108-
// names as path segments and the result hits 404. Narrow layouts skip the deep-restore
109-
// and go to the workspace's initial page (mirrors mobile behavior).
110-
const wrappedState =
111-
!shouldUseNarrowLayout && workspacesTabState
112-
? wrapStateInNavigators(workspacesTabState as PartialState<NavigationState>, [
113-
NAVIGATORS.TAB_NAVIGATOR,
114-
NAVIGATORS.WORKSPACE_NAVIGATOR,
115-
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
116-
])
117-
: undefined;
118-
const targetPath = (wrappedState ? getPathFromState(wrappedState) : ROUTES.WORKSPACE_INITIAL.getRoute(policyID)) as Route;
119-
Navigation.navigate(targetPath);
106+
107+
if (policy?.id) {
108+
// Synthesize a URL from the captured WorkspaceSplitNavigator inner state and navigate
109+
// to it. URL-based navigation goes through `getStateFromPath`, which produces a fully
110+
// formed nested state and reliably handles pushing a fresh TabNavigator on top of an
111+
// existing fullscreen stack. The state has to be wrapped with its full ancestor chain
112+
// (TAB_NAVIGATOR > WORKSPACE_NAVIGATOR > WORKSPACE_SPLIT_NAVIGATOR) so `getPathFromState`
113+
// can match the linking-config hierarchy and produce a real URL like
114+
// `/workspaces/POLICY_ID/workflows`; otherwise the resolver falls back to navigator
115+
// names as path segments and the result hits 404. Narrow layouts skip the deep-restore
116+
// and go to the workspace's initial page (mirrors mobile behavior).
117+
const wrappedState =
118+
!shouldUseNarrowLayout && workspacesTabState
119+
? wrapStateInNavigators(workspacesTabState as PartialState<NavigationState>, [
120+
NAVIGATORS.TAB_NAVIGATOR,
121+
NAVIGATORS.WORKSPACE_NAVIGATOR,
122+
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
123+
])
124+
: undefined;
125+
const targetPath = (wrappedState ? getPathFromState(wrappedState) : ROUTES.WORKSPACE_INITIAL.getRoute(policy.id)) as Route;
126+
Navigation.navigate(targetPath);
127+
}
120128
return;
121129
}
122130

123-
// Domain route found: navigate to last domain screen by ID.
131+
// Domain route found: try to restore last domain screen.
124132
if (lastWorkspacesTabNavigatorRoute.name === NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR) {
125-
if (domainAccountID !== undefined) {
126-
Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(domainAccountID));
133+
if (domain?.accountID !== undefined) {
134+
Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(domain.accountID));
127135
return;
128136
}
129137
}

tests/unit/navigateToWorkspacesPageTest.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser';
22
import getPathFromState from '@libs/Navigation/helpers/getPathFromState';
33
import navigateToWorkspacesPage from '@libs/Navigation/helpers/navigateToWorkspacesPage';
44
import Navigation from '@libs/Navigation/Navigation';
5+
import * as PolicyUtils from '@libs/PolicyUtils';
56
import NAVIGATORS from '@src/NAVIGATORS';
67
import ROUTES from '@src/ROUTES';
78
import SCREENS from '@src/SCREENS';
9+
import createRandomPolicy from '../utils/collections/policies';
810

911
jest.mock('@libs/Navigation/navigationRef');
1012
jest.mock('@libs/Navigation/Navigation');
1113
jest.mock('@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState');
14+
jest.mock('@libs/PolicyUtils');
1215
jest.mock('@libs/interceptAnonymousUser');
1316
jest.mock('@libs/Navigation/helpers/getPathFromState', () => ({
1417
__esModule: true,
@@ -18,7 +21,8 @@ jest.mock('@libs/Navigation/helpers/getPathFromState', () => ({
1821
const mockedGetPathFromState = getPathFromState as jest.MockedFunction<typeof getPathFromState>;
1922

2023
const fakePolicyID = '344559B2CCF2B6C1';
21-
const baseParams = {shouldUseNarrowLayout: false, policyID: fakePolicyID};
24+
const mockPolicy = {...createRandomPolicy(0), id: fakePolicyID};
25+
const baseParams = {currentUserLogin: 'test@example.com', shouldUseNarrowLayout: false, policy: mockPolicy};
2226

2327
describe('navigateToWorkspacesPage', () => {
2428
beforeEach(() => {
@@ -63,6 +67,9 @@ describe('navigateToWorkspacesPage', () => {
6367
});
6468

6569
it('navigates to the workspace initial URL when no workspacesTabState is provided', () => {
70+
(PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(true);
71+
(PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(false);
72+
6673
mockIntercept();
6774
navigateToWorkspacesPage({
6875
...baseParams,
@@ -75,6 +82,8 @@ describe('navigateToWorkspacesPage', () => {
7582
});
7683

7784
it('navigates to the URL produced by getPathFromState when workspacesTabState is provided on wide layouts', () => {
85+
(PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(true);
86+
(PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(false);
7887
const restoredPath = `/workspaces/${fakePolicyID}/workflows` as const;
7988
mockedGetPathFromState.mockReturnValue(restoredPath);
8089

@@ -119,6 +128,9 @@ describe('navigateToWorkspacesPage', () => {
119128
});
120129

121130
it('falls back to the workspace initial URL on narrow layouts even when workspacesTabState is provided', () => {
131+
(PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(true);
132+
(PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(false);
133+
122134
mockIntercept();
123135
navigateToWorkspacesPage({
124136
...baseParams,
@@ -138,11 +150,39 @@ describe('navigateToWorkspacesPage', () => {
138150
expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_INITIAL.getRoute(fakePolicyID));
139151
});
140152

153+
it('navigates to WORKSPACES_LIST if policy is pending delete', () => {
154+
(PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(true);
155+
(PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(true);
156+
157+
mockIntercept();
158+
navigateToWorkspacesPage({
159+
...baseParams,
160+
topmostFullScreenRoute: {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR},
161+
lastWorkspacesTabNavigatorRoute: {name: NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR},
162+
});
163+
164+
expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACES_LIST.route);
165+
});
166+
167+
it('navigates to WORKSPACES_LIST if shouldShowPolicy is false for the user', () => {
168+
(PolicyUtils.shouldShowPolicy as jest.Mock).mockReturnValue(false);
169+
(PolicyUtils.isPendingDeletePolicy as jest.Mock).mockReturnValue(false);
170+
171+
mockIntercept();
172+
navigateToWorkspacesPage({
173+
...baseParams,
174+
topmostFullScreenRoute: {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR},
175+
lastWorkspacesTabNavigatorRoute: {name: NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR},
176+
});
177+
178+
expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACES_LIST.route);
179+
});
180+
141181
it('navigates to WORKSPACES_LIST if policyID is missing', () => {
142182
mockIntercept();
143183
navigateToWorkspacesPage({
144184
...baseParams,
145-
policyID: undefined,
185+
policy: undefined,
146186
topmostFullScreenRoute: {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR},
147187
lastWorkspacesTabNavigatorRoute: {name: NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR},
148188
});

0 commit comments

Comments
 (0)