Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

STCOR-952 break QueryStateUpdates out to their own class-based component. #1604

Merged
merged 9 commits into from
Mar 4, 2025
2 changes: 2 additions & 0 deletions src/RootWithIntl.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
AppCtxMenuProvider,
SessionEventContainer,
AppOrderProvider,
QueryStateUpdater
} from './components';
import StaleBundleWarning from './components/StaleBundleWarning';
import { StripesContext } from './StripesContext';
Expand Down Expand Up @@ -64,6 +65,7 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut
<Router history={history}>
{ isAuthenticated || token || disableAuth ?
<>
<QueryStateUpdater stripes={connectedStripes} queryClient={queryClient} />
<MainContainer>
<AppCtxMenuProvider>
<AppOrderProvider>
Expand Down
1 change: 1 addition & 0 deletions src/RootWithIntl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jest.mock('./components/ModuleContainer', () => ({ children }) => children);
jest.mock('./components/MainContainer', () => ({ children }) => children);
jest.mock('./components/StaleBundleWarning', () => () => '<StaleBundleWarning>');
jest.mock('./components/SessionEventContainer', () => () => '<SessionEventContainer>');
jest.mock('./components/MainNav/QueryStateUpdater', () => () => null);

const defaultHistory = createMemoryHistory();

Expand Down
55 changes: 1 addition & 54 deletions src/components/MainNav/MainNav.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import React, { useEffect, useRef, useState } from 'react';
import { isEqual } from 'lodash';
import { useIntl } from 'react-intl';
import { useLocation, useHistory } from 'react-router-dom';
import { useQueryClient } from 'react-query';

import { branding } from 'stripes-config';
import { Icon } from '@folio/stripes-components';
import {
updateQueryResource,
getLocationQuery,
updateLocation,
getCurrentModule,
isQueryResourceModule,
getQueryResourceState,
} from '../../locationService';

import css from './MainNav.css';
import NavButton from './NavButton';
Expand All @@ -22,65 +11,23 @@ import { CurrentAppGroup } from './CurrentApp';
import ProfileDropdown from './ProfileDropdown';
import AppList from './AppList';
import { SkipLink } from './components';

import { useAppOrderContext } from './AppOrderProvider';

import { useStripes } from '../../StripesContext';
import { useModules } from '../../ModulesContext';

const MainNav = () => {
const {
apps,
} = useAppOrderContext();
const queryClient = useQueryClient();
const stripes = useStripes();
const location = useLocation();
const modules = useModules();
const history = useHistory();
const intl = useIntl();

const [curModule, setCurModule] = useState(getCurrentModule(modules, location));
const [selectedApp, setSelectedApp] = useState(apps.find(entry => entry.active));
const helpUrl = useRef(stripes.config.helpUrl ?? 'https://docs.folio.org').current;

useEffect(() => {
let curQuery = getLocationQuery(location);
const prevQueryState = {};

const { store } = stripes;
const _unsubscribe = store.subscribe(() => {
const module = curModule;

if (module && isQueryResourceModule(module, location)) {
const { moduleName } = module;
const queryState = getQueryResourceState(module, stripes.store);

// only update location if query state has changed
if (!isEqual(queryState, prevQueryState[moduleName])) {
curQuery = updateLocation(module, curQuery, stripes.store, history, location);
prevQueryState[moduleName] = queryState;
}
}
});

// remove QueryProvider cache to be 100% sure we're starting from a clean slate.
queryClient.removeQueries();

return () => {
_unsubscribe();
};
}, [location]); // eslint-disable-line

// if the location changes, we need to update the current module/query resource.
// This logic changes the visible current app at the starting side of the Main Navigation.
useEffect(() => {
setSelectedApp(apps.find(entry => entry.active));
const nextCurModule = getCurrentModule(modules, location);
if (nextCurModule) {
setCurModule(getCurrentModule(modules, location));
updateQueryResource(location, nextCurModule, stripes.store);
}
}, [modules, location, stripes.store, apps]);
}, [apps]);

return (
<header className={css.navRoot} style={branding?.style?.mainNav ?? {}}>
Expand Down
98 changes: 98 additions & 0 deletions src/components/MainNav/QueryStateUpdater.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';
import PropTypes from 'prop-types';
import { compose } from 'redux';
import { injectIntl } from 'react-intl';
import { withRouter } from 'react-router';
import { isEqual, find } from 'lodash';

import { withModules } from '../Modules';

import {
updateQueryResource,
getLocationQuery,
updateLocation,
getCurrentModule,
isQueryResourceModule,
getQueryResourceState,
} from '../../locationService';

// onMount of stripes, sync the query state to the location.
class QueryStateUpdater extends React.Component {
static propTypes = {
history: PropTypes.shape({
listen: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired,
push: PropTypes.func.isRequired,
}).isRequired,
location: PropTypes.shape({
pathname: PropTypes.string,
}).isRequired,
modules: PropTypes.shape({
app: PropTypes.arrayOf(PropTypes.object),
}),
queryClient: PropTypes.shape({
removeQueries: PropTypes.func.isRequired,
}).isRequired,
stripes: PropTypes.shape({
store: PropTypes.shape({
subscribe: PropTypes.func.isRequired,
}),
}),
}

constructor(props) {
super(props);
this.store = props.stripes.store;
}

componentDidMount() {
let curQuery = getLocationQuery(this.props.location);
const prevQueryState = {};

this._unsubscribe = this.store.subscribe(() => {
const { history, location } = this.props;
const module = this.curModule;
const state = this.store.getState();

// If user has timed out, force them to log in again.
if (state?.okapi?.token && state.okapi.authFailure
&& find(state.okapi.authFailure, { type: 'error', code: 'user.timeout' })) {
this.returnToLogin();
}

if (module && isQueryResourceModule(module, location)) {
const { moduleName } = module;
const queryState = getQueryResourceState(module, this.store);

// only update location if query state has changed
if (!isEqual(queryState, prevQueryState[moduleName])) {
curQuery = updateLocation(module, curQuery, this.store, history, location);
prevQueryState[moduleName] = queryState;
}
}
});

// remove QueryProvider cache to be 100% sure we're starting from a clean slate.
this.props.queryClient.removeQueries();
}

componentDidUpdate(prevProps) {
const { modules, location } = this.props;
this.curModule = getCurrentModule(modules, location);
if (this.curModule && !isEqual(location, prevProps.location)) {
updateQueryResource(location, this.curModule, this.store);
}
}

componentWillUnmount() {
this._unsubscribe();
}

render = () => this.props.children;

Check failure on line 91 in src/components/MainNav/QueryStateUpdater.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

'children' is missing in props validation
};

Check failure on line 92 in src/components/MainNav/QueryStateUpdater.js

View workflow job for this annotation

GitHub Actions / ui / Install and lint / Install and lint

Unnecessary semicolon

export default compose(
injectIntl,
withRouter,
withModules,
)(QueryStateUpdater);
1 change: 1 addition & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { default as Login } from './Login';
export { default as Logout } from './Logout';
export { default as MainContainer } from './MainContainer';
export { default as MainNav } from './MainNav';
export { default as QueryStateUpdater } from './MainNav/QueryStateUpdater';
export { AppOrderProvider } from './MainNav/AppOrderProvider';
export { default as ModuleContainer } from './ModuleContainer';
export { withModule, withModules } from './Modules';
Expand Down
Loading