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-936 - implement App-reordering, user preference management in stripes-core. #1584

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b27ae44
add permissions for user preference management, add okapiInterface
JohnC-80 Jan 27, 2025
662db88
add usePreferences hook
JohnC-80 Jan 27, 2025
92aa6e1
add AppOrderProvider context
JohnC-80 Jan 27, 2025
cab6928
implement App Reordering, refactor MainNav to functional.
JohnC-80 Jan 27, 2025
c36e6cf
Merge branch 'master' into STCOR-936
JohnC-80 Jan 27, 2025
c7bf306
recreat apps list on pathname change - fixes 'selectedApp'
JohnC-80 Jan 28, 2025
2ad7943
Merge branch 'STCOR-936' of https://github.com/folio-org/stripes-core…
JohnC-80 Jan 28, 2025
a78b938
AppOrderProvider: key query on user id
JohnC-80 Jan 28, 2025
26b3037
AppOrderProvider - key query on user id
JohnC-80 Jan 28, 2025
80f4a85
AppOrderProvider: clean comments
JohnC-80 Jan 28, 2025
23e31bd
add tests for usePreferences hook
JohnC-80 Jan 29, 2025
0c6e189
AppOrderProvider tests
JohnC-80 Jan 29, 2025
1d2a3dc
more AppOrderProvider tests
JohnC-80 Jan 30, 2025
75679bf
usePreferences clean up
JohnC-80 Jan 30, 2025
03fd9f7
AppList clean-up
JohnC-80 Jan 30, 2025
27fbb0c
Generalized perm descriptions for managing user preferences
JohnC-80 Jan 30, 2025
cde0ee5
AppOrderProvider clean-up
JohnC-80 Jan 30, 2025
de63069
clean up usePreference parameters
JohnC-80 Jan 30, 2025
3beaa16
clean up tests for usePreferences, AppOrderProvider
JohnC-80 Jan 30, 2025
844116e
Merge branch 'master' into STCOR-936
JohnC-80 Jan 30, 2025
6c8d962
use 'act' from testing-library/react
JohnC-80 Jan 30, 2025
88d5756
Merge branch 'STCOR-936' of https://github.com/folio-org/stripes-core…
JohnC-80 Jan 30, 2025
8b732b5
Merge branch 'master' into STCOR-936
JohnC-80 Jan 31, 2025
cbebda2
export AppImportProvider directly from itself rather than through Mai…
JohnC-80 Feb 3, 2025
38248a5
Merge branch 'STCOR-936' of https://github.com/folio-org/stripes-core…
JohnC-80 Feb 3, 2025
0c701c4
minor lint
JohnC-80 Feb 3, 2025
dfe2520
Merge branch 'master' into STCOR-936
JohnC-80 Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as useOkapiKy } from './src/useOkapiKy';
export { default as withOkapiKy } from './src/withOkapiKy';
export { default as useCustomFields } from './src/useCustomFields';
export { default as createReactQueryClient } from './src/createReactQueryClient';
export { useAppOrderContext } from './src/components/MainNav/AppOrderProvider';

/* components */
export { default as AppContextMenu } from './src/components/MainNav/CurrentApp/AppContextMenu';
Expand Down
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"okapiInterfaces": {
"users-bl": "5.0 6.0",
"authtoken": "1.0 2.0",
"configuration": "2.0"
"configuration": "2.0",
"settings": "1.0"
},
"optionalOkapiInterfaces": {
"consortia": "1.0",
Expand All @@ -38,6 +39,16 @@
{
"permissionName": "settings.enabled",
"displayName": "UI: settings area is enabled"
},
{
"permissionName": "mod-settings.owner.read.stripes-core.prefs.manage",
"displayName": "UI: read the user's own preference for the order of links in the main navigation.",
JohnC-80 marked this conversation as resolved.
Show resolved Hide resolved
"visible": false
},
{
"permissionName": "mod-settings.owner.write.stripes-core.prefs.manage",
"displayName": "UI: update the user's own preference for the order of links in the main navigation.",
JohnC-80 marked this conversation as resolved.
Show resolved Hide resolved
"visible": false
}
]
},
Expand Down
101 changes: 52 additions & 49 deletions src/RootWithIntl.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
ForgotUserNameCtrl,
AppCtxMenuProvider,
SessionEventContainer,
AppOrderProvider,
} from './components';
import StaleBundleWarning from './components/StaleBundleWarning';
import { StripesContext } from './StripesContext';
Expand Down Expand Up @@ -65,55 +66,57 @@ const RootWithIntl = ({ stripes, token = '', isAuthenticated = false, disableAut
<>
<MainContainer>
<AppCtxMenuProvider>
<MainNav stripes={connectedStripes} queryClient={queryClient} />
{typeof connectedStripes?.config?.staleBundleWarning === 'object' && <StaleBundleWarning />}
<HandlerManager
event={events.LOGIN}
stripes={connectedStripes}
/>
{ (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && (
<ModuleContainer id="content">
<OverlayContainer />
{connectedStripes.config.useSecureTokens && <SessionEventContainer history={history} queryClient={queryClient} />}
<Switch>
<TitledRoute
name="home"
path="/"
key="root"
exact
component={<Front stripes={connectedStripes} />}
/>
<TitledRoute
name="ssoRedirect"
path="/sso-landing"
key="sso-landing"
component={<SSORedirect stripes={connectedStripes} />}
/>
<TitledRoute
name="oidcRedirect"
path="/oidc-landing"
key="oidc-landing"
component={<OIDCRedirect stripes={stripes} />}
/>
<TitledRoute
name="logoutTimeout"
path="/logout-timeout"
component={<Logout />}
/>
<TitledRoute
name="settings"
path="/settings"
component={<Settings stripes={connectedStripes} />}
/>
<TitledRoute
name="logout"
path="/logout"
component={<Logout />}
/>
<ModuleRoutes stripes={connectedStripes} />
</Switch>
</ModuleContainer>
)}
<AppOrderProvider>
<MainNav stripes={connectedStripes} queryClient={queryClient} />
{typeof connectedStripes?.config?.staleBundleWarning === 'object' && <StaleBundleWarning />}
<HandlerManager
event={events.LOGIN}
stripes={connectedStripes}
/>
{ (typeof connectedStripes.okapi !== 'object' || connectedStripes.discovery.isFinished) && (
<ModuleContainer id="content">
<OverlayContainer />
{connectedStripes.config.useSecureTokens && <SessionEventContainer history={history} queryClient={queryClient} />}
<Switch>
<TitledRoute
name="home"
path="/"
key="root"
exact
component={<Front stripes={connectedStripes} />}
/>
<TitledRoute
name="ssoRedirect"
path="/sso-landing"
key="sso-landing"
component={<SSORedirect stripes={connectedStripes} />}
/>
<TitledRoute
name="oidcRedirect"
path="/oidc-landing"
key="oidc-landing"
component={<OIDCRedirect stripes={stripes} />}
/>
<TitledRoute
name="logoutTimeout"
path="/logout-timeout"
component={<Logout />}
/>
<TitledRoute
name="settings"
path="/settings"
component={<Settings stripes={connectedStripes} />}
/>
<TitledRoute
name="logout"
path="/logout"
component={<Logout />}
/>
<ModuleRoutes stripes={connectedStripes} />
</Switch>
</ModuleContainer>
)}
</AppOrderProvider>
</AppCtxMenuProvider>
</MainContainer>
<Callout ref={setCalloutDomRef} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/MainNav/AppList/AppList.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class AppList extends Component {
{
apps.map(app => {
const isHidden = hiddenItemIds.includes(app.id);

// const isHidden = false;
JohnC-80 marked this conversation as resolved.
Show resolved Hide resolved
return (
<li
className={classnames(css.navItem, { [css.hidden]: isHidden })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import React from 'react';
import PropTypes from 'prop-types';
import sortBy from 'lodash/sortBy';

Check warning on line 7 in src/components/MainNav/AppList/components/AppListDropdown/AppListDropdown.js

View workflow job for this annotation

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

'sortBy' is defined but never used. Allowed unused vars must match /React/u

import { NavListItem, NavListSection } from '@folio/stripes-components';

Expand All @@ -20,7 +20,7 @@
striped
>
{
sortBy(apps, app => app.displayName.toLowerCase()).map(app => (
apps.map(app => (
<NavListItem
key={app.id}
data-test-app-list-dropdown-item
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import debounce from 'lodash/debounce';
import PropTypes from 'prop-types';
import css from './ResizeContainer.css';
import isEqual from 'lodash/isEqual';

Check failure on line 10 in src/components/MainNav/AppList/components/ResizeContainer/ResizeContainer.js

View workflow job for this annotation

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

`lodash/isEqual` import should occur before import of `./ResizeContainer.css`

Check warning on line 10 in src/components/MainNav/AppList/components/ResizeContainer/ResizeContainer.js

View workflow job for this annotation

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

'isEqual' is defined but never used. Allowed unused vars must match /React/u

class ResizeContainer extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -40,9 +41,12 @@
}

componentDidUpdate(prevProps) {
const { currentAppId, items } = this.props;
// Update hidden items when the current app ID changes
// to make sure that no items are hidden behind the current app label
if (this.props.currentAppId && this.props.currentAppId !== prevProps.currentAppId) {
if (currentAppId !== prevProps.currentAppId ||
items !== prevProps.items
) {
this.updateHiddenItems();
}
}
Expand Down Expand Up @@ -79,7 +83,7 @@
* Determine hidden items on mount and resize
*/
updateHiddenItems = (callback) => {
const { hideAllWidth, offset } = this.props;
const { hideAllWidth, offset, items } = this.props;
const { cachedItemWidths } = this.state;
const shouldHideAll = window.innerWidth <= hideAllWidth;
const wrapperEl = this.wrapperRef.current;
Expand All @@ -92,7 +96,7 @@
shouldHideAll ? Object.keys(cachedItemWidths) :

// Find items that should be hidden
Object.keys(cachedItemWidths).reduce((acc, id) => {
items.reduce((acc, { id }) => {
const itemWidth = cachedItemWidths[id];
const shouldBeHidden = (itemWidth + acc.accWidth + offset) > wrapperWidth;
const hidden = shouldBeHidden ? acc.hidden.concat(id) : acc.hidden;
Expand All @@ -101,7 +105,8 @@
hidden,
accWidth: acc.accWidth + itemWidth,
};
}, {
},
{
hidden: [],
accWidth: 0,
}).hidden;
Expand Down
164 changes: 164 additions & 0 deletions src/components/MainNav/AppOrderProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { createContext, useContext, useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { useQuery } from 'react-query';

import { useStripes } from '../../StripesContext';
import { useModules } from '../../ModulesContext';
import { LastVisitedContext } from '../LastVisited';
import usePreferences from '../../hooks/usePreferences';
import { packageName } from '../../constants';
import settingsIcon from './settings.svg';


const APPORDER_PREF_NAME = 'user-main-nav-order';
const APPORDER_PREF_SCOPE = 'stripes-core.prefs.manage';

export const AppOrderContext = createContext({
isLoading: true,
listOrder: [],
apps: [],
updateList: () => {},
reset: () => {},
});

export const useAppOrderContext = () => {
return useContext(AppOrderContext);
};

function getProvisionedApps(appModules, stripes, pathname, lastVisited, formatMessage) {
const apps = appModules.map((entry) => {
const name = entry.module.replace(packageName.PACKAGE_SCOPE_REGEX, '');
const perm = `module.${name}.enabled`;

if (!stripes.hasPerm(perm)) {
return null;
}

const id = `clickable-${name}-module`;

const pathRoot = pathname.split('/')[1];
const entryRoot = entry.route.split('/')[1];
JohnC-80 marked this conversation as resolved.
Show resolved Hide resolved
const active = pathRoot === entryRoot;

const last = lastVisited[name];
const home = entry.home || entry.route;
const href = (active || !last) ? home : lastVisited[name];

return {
id,
href,
active,
name,
...entry,
};
}).filter(app => app);

/**
* Add Settings to apps array manually
* until Settings becomes a standalone app
*/

if (stripes.hasPerm('settings.enabled')) {
apps.push({
displayName: formatMessage({ id: 'stripes-core.settings' }),
name: 'settings',
id: 'clickable-settings',
href: lastVisited.x_settings || '/settings',
active: pathname.startsWith('/settings'),
description: 'FOLIO settings',
iconData: {
src: settingsIcon,
alt: 'Tenant Settings',
title: 'Settings',
},
route: '/settings'
});
}
return apps;
}

export const AppOrderProvider = ({ children }) => {

Check failure on line 81 in src/components/MainNav/AppOrderProvider.js

View workflow job for this annotation

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

'children' is missing in props validation
const { lastVisited } = useContext(LastVisitedContext);
const { app } = useModules();
const stripes = useStripes();
const { pathname } = useLocation();
const { formatMessage } = useIntl();
const { getPreference, setPreference, removePreference } = usePreferences();

const { data: userAppList, isLoading, refetch: refetchAppList } = useQuery(`${stripes?.user?.user?.id}-pref-query`, () => {
return getPreference({ key: APPORDER_PREF_NAME, scope: APPORDER_PREF_SCOPE });
JohnC-80 marked this conversation as resolved.
Show resolved Hide resolved
});

// returns list of apps in user-defined order. By alpha if no order defined.
const apps = useMemo(() => {
if (!stripes) return [];
const platformApps = getProvisionedApps(app, stripes, pathname, lastVisited, formatMessage);

let orderedApps = userAppList || []; // the persisted, user-preferred app order.
let navList = []; // contains the ultimate reordered array of app nav items.

// No length in the persisted apps means apps ordered by alpha (default)
if (!orderedApps?.length) {
// default ordered apps just contain a subset of the fields in the app object...
orderedApps = platformApps.map(({ name }) => ({ name }));
navList = platformApps;
} else {
// reorder apps to the persisted preference value...
navList = orderedApps.map((listing) => {
const { name: appName } = listing;
const appIndex = platformApps.findIndex((module) => appName === module.name);

if (appIndex !== -1) {
return platformApps[appIndex];
}

return false;
});

// find the apps from the platform that are not saved in user-reordered list and tack them on at the end.
// these cover permission changes/apps added... can be labeled as 'new' in the preference settings.
platformApps.forEach((platApp) => {
const orderedIndex = orderedApps.findIndex((oa) => oa.name === platApp.name);
if (orderedIndex === -1) {
orderedApps.push({
name: platApp.name,
isNew: true,
});
}
});
}

return { navList, orderedApps };
}, [formatMessage, app, lastVisited, userAppList, pathname]); // omitted: stripes

Check warning on line 133 in src/components/MainNav/AppOrderProvider.js

View workflow job for this annotation

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

React Hook useMemo has a missing dependency: 'stripes'. Either include it or remove the dependency array

const updateList = async (list) => {
// clean the 'isNew' field;
list.forEach((item) => {
if (Object.prototype.hasOwnProperty.call(item, 'isNew')) {
delete item.isNew;
}
});
await setPreference({ key: APPORDER_PREF_NAME, scope: APPORDER_PREF_SCOPE, value: list });
await refetchAppList();
};

// resetting the app order preference just removes the entry from settings.
const reset = async () => {
await removePreference({ key: APPORDER_PREF_NAME, scope: APPORDER_PREF_SCOPE });
await refetchAppList();
};

return (
<AppOrderContext.Provider value={{
isLoading,
apps: apps.navList,
appNavOrder: apps.orderedApps,
updateList,
reset
}}
>
{children}
</AppOrderContext.Provider>
);
};
Loading
Loading