Skip to content

Commit

Permalink
add state diff for navigation in rhp
Browse files Browse the repository at this point in the history
  • Loading branch information
adamgrzybowski committed Jan 26, 2024
1 parent 3abdd8b commit c4b9e41
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 2 deletions.
43 changes: 43 additions & 0 deletions src/libs/Navigation/AppNavigator/getActionsFromPartialDiff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {getActionFromState, StackActions} from '@react-navigation/native';
import type {NavigationAction} from '@react-navigation/native';
import linkingConfig from '@libs/Navigation/linkingConfig';
import NAVIGATORS from '@src/NAVIGATORS';
import type {GetPartialStateDiffReturnType} from './getPartialStateDiff';

/**
* @param diff - Diff generated by getPartialDiff.
* @returns Array of actions to dispatch to apply diff.
*/
function getActionsFromPartialDiff(diff: GetPartialStateDiffReturnType): NavigationAction[] {
const actions: NavigationAction[] = [];

const bottomTabDiff = diff[NAVIGATORS.BOTTOM_TAB_NAVIGATOR];
const centralPaneDiff = diff[NAVIGATORS.CENTRAL_PANE_NAVIGATOR];
const fullScreenDiff = diff[NAVIGATORS.FULL_SCREEN_NAVIGATOR];

// There is only one bottom tab navigator so we can just push this route.
if (bottomTabDiff) {
actions.push(StackActions.push(bottomTabDiff.name, bottomTabDiff.params));
}

if (centralPaneDiff) {
// In this case we have to wrap the inner central pane route with central pane navigator.
actions.push(
StackActions.push(NAVIGATORS.CENTRAL_PANE_NAVIGATOR, {
screen: centralPaneDiff.name,
params: centralPaneDiff.params,
}),
);
}

if (fullScreenDiff) {
const action = getActionFromState({routes: [fullScreenDiff]}, linkingConfig.config);
if (action) {
actions.push(action);
}
}

return actions;
}

export default getActionsFromPartialDiff;
77 changes: 77 additions & 0 deletions src/libs/Navigation/AppNavigator/getPartialStateDiff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import getTopmostBottomTabRoute from '@libs/Navigation/getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from '@libs/Navigation/getTopmostCentralPaneRoute';
import type {Metainfo} from '@libs/Navigation/linkingConfig/getAdaptedStateFromPath';
import type {NavigationPartialRoute, RootStackParamList, State} from '@libs/Navigation/types';
import NAVIGATORS from '@src/NAVIGATORS';

// eslint-disable-next-line @typescript-eslint/ban-types
const shallowCompare = (obj1?: object, obj2?: object) => {
if (!obj1 && !obj2) {
return true;
}
if (obj1 && obj2) {
// @ts-expect-error we know that obj1 and obj2 are params of a route.
return Object.keys(obj1).length === Object.keys(obj2).length && Object.keys(obj1).every((key) => obj1[key] === obj2[key]);
}
return false;
};

type GetPartialStateDiffReturnType = {
[NAVIGATORS.BOTTOM_TAB_NAVIGATOR]?: NavigationPartialRoute;
[NAVIGATORS.CENTRAL_PANE_NAVIGATOR]?: NavigationPartialRoute;
[NAVIGATORS.FULL_SCREEN_NAVIGATOR]?: NavigationPartialRoute;
};

/**
* This function returns partial additive diff beteween the two states.
* The partial diff have information which bottom tab, central pane and full screen screens we need to push to go from state to templateState
* @param state - Current state.
* @param templateState - Desired state generated with getAdaptedStateFromPath.
* @param metainfo - Additional info from getAdaptedStateFromPath funciton.
* @returns The screen options object
*/
function getPartialStateDiff(state: State<RootStackParamList>, templateState: State<RootStackParamList>, metainfo: Metainfo): GetPartialStateDiffReturnType {
const diff: GetPartialStateDiffReturnType = {};

// If it is mandatory we need to compare both central pane and bottom tab of states.
if (metainfo.isCentralPaneAndBottomTabMandatory) {
const stateTopmostBottomTab = getTopmostBottomTabRoute(state);
const templateStateTopmostBottomTab = getTopmostBottomTabRoute(templateState);

// Bottom tab navigator
if (stateTopmostBottomTab && templateStateTopmostBottomTab && stateTopmostBottomTab.name !== templateStateTopmostBottomTab.name) {
diff[NAVIGATORS.BOTTOM_TAB_NAVIGATOR] = templateStateTopmostBottomTab;
}

const stateTopmostCentralPane = getTopmostCentralPaneRoute(state);
const templateStateTopmostCentralPane = getTopmostCentralPaneRoute(templateState);

if (
// If the central pane is only in the template state, it's diff.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(!stateTopmostCentralPane && templateStateTopmostCentralPane) ||
(stateTopmostCentralPane &&
templateStateTopmostCentralPane &&
stateTopmostCentralPane.name !== templateStateTopmostCentralPane.name &&
!shallowCompare(stateTopmostCentralPane.params, templateStateTopmostCentralPane.params))
) {
// We need to wrap central pane routes in the central pane navigator.
diff[NAVIGATORS.CENTRAL_PANE_NAVIGATOR] = templateStateTopmostCentralPane;
}
}

// This one is heurestic and may need to improved if we will be able to navigate from modal screen with full screen in background to another modal screen with full screen in background.
// For now this simple check is enought.
if (metainfo.isFullScreenNavigatorMandatory) {
const stateTopmostFullScreen = state.routes.filter((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR).at(-1);
const templateStateTopmostFullScreen = templateState.routes.filter((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR).at(-1) as NavigationPartialRoute;
if (!stateTopmostFullScreen && templateStateTopmostFullScreen) {
diff[NAVIGATORS.FULL_SCREEN_NAVIGATOR] = templateStateTopmostFullScreen;
}
}

return diff;
}

export default getPartialStateDiff;
export type {GetPartialStateDiffReturnType};
13 changes: 13 additions & 0 deletions src/libs/Navigation/linkTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import type {Route} from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import getActionsFromPartialDiff from './AppNavigator/getActionsFromPartialDiff';
import getPartialStateDiff from './AppNavigator/getPartialStateDiff';
import dismissModal from './dismissModal';
import getPolicyIdFromState from './getPolicyIdFromState';
import getStateFromPath from './getStateFromPath';
import getTopmostBottomTabRoute from './getTopmostBottomTabRoute';
import getTopmostCentralPaneRoute from './getTopmostCentralPaneRoute';
import getTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
import getAdaptedStateFromPath from './linkingConfig/getAdaptedStateFromPath';
import getMatchingBottomTabRouteForState from './linkingConfig/getMatchingBottomTabRouteForState';
import getMatchingCentralPaneRouteForState from './linkingConfig/getMatchingCentralPaneRouteForState';
import replacePathInNestedState from './linkingConfig/replacePathInNestedState';
Expand Down Expand Up @@ -185,6 +188,16 @@ export default function linkTo(navigation: NavigationContainerRef<RootStackParam
dismissModal('', navigation);
}
action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH;

// If this RHP has mandatory central pane and bottom tab screens defined we need to push them.
const {adaptedState, metainfo} = getAdaptedStateFromPath(path, linkingConfig.config);
if (adaptedState && (metainfo.isCentralPaneAndBottomTabMandatory || metainfo.isFullScreenNavigatorMandatory)) {
const diff = getPartialStateDiff(rootState, adaptedState as State<RootStackParamList>, metainfo);
const diffActions = getActionsFromPartialDiff(diff);
for (const diffAction of diffActions) {
root.dispatch(diffAction);
}
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
} else if (action.payload.name === NAVIGATORS.BOTTOM_TAB_NAVIGATOR) {
// If path contains a policyID, we should invoke the navigate function
Expand Down
16 changes: 14 additions & 2 deletions src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Metainfo = {
// If the screens in the bottom tab and central pane are not mandatory for this state, we want to have this information.
// It will help us later with creating proper diff betwen current and desired state.
isCentralPaneAndBottomTabMandatory: boolean;
isFullScreenNavigatorMandatory: boolean;
};

type GetAdaptedStateReturnType = {
Expand Down Expand Up @@ -134,6 +135,7 @@ function getAdaptedState(state: PartialState<NavigationState<RootStackParamList>
const isSmallScreenWidth = getIsSmallScreenWidth();
const metainfo = {
isCentralPaneAndBottomTabMandatory: true,
isFullScreenNavigatorMandatory: true,
};

// We need to check what is defined to know what we need to add.
Expand All @@ -158,6 +160,7 @@ function getAdaptedState(state: PartialState<NavigationState<RootStackParamList>
// This may happen if this RHP doens't have a route that should be under the overlay defined.
if (!matchingRootRoute) {
metainfo.isCentralPaneAndBottomTabMandatory = false;
metainfo.isFullScreenNavigatorMandatory = false;
matchingRootRoute = createCentralPaneNavigator({name: SCREENS.REPORT});
}
// If the root route is type of FullScreenNavigator, the default bottom tab will be added.
Expand All @@ -177,7 +180,10 @@ function getAdaptedState(state: PartialState<NavigationState<RootStackParamList>
// - default bottom tab
// - default central pane on desktop layout
// - found lhp

// Currently there is only the search and workspace switcher in LHP both can have any central pane under the overlay.
metainfo.isCentralPaneAndBottomTabMandatory = false;
metainfo.isFullScreenNavigatorMandatory = false;
const routes = [];
routes.push(
createBottomTabNavigator(
Expand Down Expand Up @@ -206,6 +212,10 @@ function getAdaptedState(state: PartialState<NavigationState<RootStackParamList>
// - default bottom tab
// - default central pane on desktop layout
// - found fullscreen

// Full screen navigator can have any central pane and bottom tab under. They will be covered anyway.
metainfo.isCentralPaneAndBottomTabMandatory = false;

const routes = [];
routes.push(
createBottomTabNavigator(
Expand Down Expand Up @@ -271,12 +281,13 @@ function getAdaptedState(state: PartialState<NavigationState<RootStackParamList>
}

const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => {
const url = getPathWithoutPolicyID(path);
const normalizedPath = !path.startsWith('/') ? `/${path}` : path;
const pathWithoutPolicyID = getPathWithoutPolicyID(normalizedPath);
const isAnonymous = isAnonymousUser();
// Anonymous users don't have access to workspaces
const policyID = isAnonymous ? undefined : extractPolicyIDFromPath(path);

const state = getStateFromPath(url, options) as PartialState<NavigationState<RootStackParamList>>;
const state = getStateFromPath(pathWithoutPolicyID, options) as PartialState<NavigationState<RootStackParamList>>;
replacePathInNestedState(state, path);

if (state === undefined) {
Expand All @@ -286,3 +297,4 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => {
};

export default getAdaptedStateFromPath;
export type {Metainfo};

0 comments on commit c4b9e41

Please sign in to comment.