Skip to content

Commit

Permalink
Merge pull request #2866 from JetyAdam/jotai_migration_activeApp
Browse files Browse the repository at this point in the history
Jotai migration activeApp
  • Loading branch information
Hyperkid123 authored Jun 28, 2024
2 parents d624293 + c30af18 commit 9b05c69
Show file tree
Hide file tree
Showing 17 changed files with 251 additions and 153 deletions.
2 changes: 2 additions & 0 deletions src/chrome/create-chrome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ describe('create chrome', () => {
setPageMetadata: jest.fn(),
useGlobalFilter: jest.fn(),
registerModule: jest.fn(),
addNavListener: jest.fn(),
deleteNavListener: jest.fn(),
};
beforeAll(() => {
const mockAuthMethods = {
Expand Down
19 changes: 13 additions & 6 deletions src/chrome/create-chrome.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { createFetchPermissionsWatcher } from '../auth/fetchPermissions';
import { AppNavigationCB, ChromeAPI, GenericCB, NavDOMEvent } from '@redhat-cloud-services/types';
import { AppNavigationCB, ChromeAPI, GenericCB } from '@redhat-cloud-services/types';
import { Store } from 'redux';
import { AnalyticsBrowser } from '@segment/analytics-next';
import get from 'lodash/get';
import Cookies from 'js-cookie';

import {
AppNavClickItem,
appAction,
appNavClick,
appObjectId,
globalFilterScope,
removeGlobalFilter,
Expand Down Expand Up @@ -37,6 +35,7 @@ import requestPdf from '../pdf/requestPdf';
import chromeStore from '../state/chromeStore';
import { isFeedbackModalOpenAtom } from '../state/atoms/feedbackModalAtom';
import { usePendoFeedback } from '../components/Feedback';
import { NavListener, activeAppAtom } from '../state/atoms/activeAppAtom';

export type CreateChromeContextConfig = {
useGlobalFilter: (callback: (selectedTags?: FlagTagsFilter) => any) => ReturnType<typeof callback>;
Expand All @@ -48,6 +47,8 @@ export type CreateChromeContextConfig = {
chromeAuth: ChromeAuthContextValue;
registerModule: (payload: RegisterModulePayload) => void;
isPreview: boolean;
addNavListener: (cb: NavListener) => number;
deleteNavListener: (id: number) => void;
};

export const createChromeContext = ({
Expand All @@ -60,14 +61,16 @@ export const createChromeContext = ({
registerModule,
chromeAuth,
isPreview,
addNavListener,
deleteNavListener,
}: CreateChromeContextConfig): ChromeAPI => {
const fetchPermissions = createFetchPermissionsWatcher(chromeAuth.getUser);
const visibilityFunctions = getVisibilityFunctions();
const dispatch = store.dispatch;
const actions = {
appAction: (action: string) => dispatch(appAction(action)),
appObjectId: (objectId: string) => dispatch(appObjectId(objectId)),
appNavClick: (item: AppNavClickItem, event?: NavDOMEvent) => dispatch(appNavClick(item, event)),
appNavClick: (item: string) => chromeStore.set(activeAppAtom, item),
globalFilterScope: (scope: string) => dispatch(globalFilterScope(scope)),
registerModule: (module: string, manifest?: string) => registerModule({ module, manifest }),
removeGlobalFilter: (isHidden: boolean) => {
Expand All @@ -76,13 +79,17 @@ export const createChromeContext = ({
},
};

const on = (type: keyof typeof PUBLIC_EVENTS, callback: AppNavigationCB | GenericCB) => {
const on = (type: keyof typeof PUBLIC_EVENTS | 'APP_NAVIGATION', callback: AppNavigationCB | GenericCB) => {
if (type === 'APP_NAVIGATION') {
const listenerId = addNavListener(callback);
return () => deleteNavListener(listenerId);
}
if (!Object.prototype.hasOwnProperty.call(PUBLIC_EVENTS, type)) {
throw new Error(`Unknown event type: ${type}`);
}

const [listener, selector] = PUBLIC_EVENTS[type];
if (type !== 'APP_NAVIGATION' && typeof selector === 'string') {
if (typeof selector === 'string') {
(callback as GenericCB)({
data: get(store.getState(), selector) || {},
});
Expand Down
81 changes: 0 additions & 81 deletions src/components/ChromeLink/ChromeLink.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import createMockStore from 'redux-mock-store';
import { MemoryRouter } from 'react-router-dom';
import ChromeLink from './ChromeLink';
import NavContext from '../Navigation/navContext';
import { APP_NAV_CLICK } from '../../redux/action-types';

const LinkContext = ({
store,
Expand Down Expand Up @@ -48,86 +47,6 @@ describe('ChromeLink', () => {
expect(getAllByTestId('router-link')).toHaveLength(1);
});

test('should dispatch appNavClick with correct actionId for top level route', () => {
const store = mockStore({
chrome: {
moduleRoutes: [],
activeModule: 'testModule',
modules: {
testModule: {},
},
},
});
const {
container: { firstElementChild: buttton },
} = render(
<LinkContext store={store}>
<ChromeLink {...testProps}>Test module link</ChromeLink>
</LinkContext>
);

act(() => {
fireEvent.click(buttton);
});

expect(store.getActions()).toEqual([
{
type: APP_NAV_CLICK,
payload: {
id: '/',
event: {
id: '/',
navId: '/',
href: '/insights/foo',
type: 'click',
target: expect.any(Element),
},
},
},
]);
});

test('should dispatch appNavClick with correct actionId for nested route', () => {
const store = mockStore({
chrome: {
moduleRoutes: [],
activeModule: 'testModule',
modules: {
testModule: {},
},
},
});
const {
container: { firstElementChild: buttton },
} = render(
<LinkContext store={store}>
<ChromeLink {...testProps} href="/insights/foo/bar">
Test module link
</ChromeLink>
</LinkContext>
);

act(() => {
fireEvent.click(buttton);
});

expect(store.getActions()).toEqual([
{
type: APP_NAV_CLICK,
payload: {
id: 'bar',
event: {
id: 'bar',
navId: 'bar',
href: '/insights/foo/bar',
type: 'click',
target: expect.any(Element),
},
},
},
]);
});

test('should not trigger onLinkClick callback', () => {
const onLinkClickSpy = jest.fn();
const store = mockStore({
Expand Down
9 changes: 4 additions & 5 deletions src/components/ChromeLink/ChromeLink.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React, { memo, useContext, useMemo, useRef } from 'react';
import { NavLink } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import { preloadModule } from '@scalprum/core';

import { appNavClick } from '../../redux/actions';
import NavContext, { OnLinkClick } from '../Navigation/navContext';
import { NavDOMEvent } from '../../@types/types';
import { useAtomValue } from 'jotai';
import { useAtomValue, useSetAtom } from 'jotai';
import { activeModuleAtom } from '../../state/atoms/activeModuleAtom';
import { moduleRoutesAtom } from '../../state/atoms/chromeModuleAtom';
import { triggerNavListenersAtom } from '../../state/atoms/activeAppAtom';

interface RefreshLinkProps extends React.HTMLAttributes<HTMLAnchorElement> {
isExternal?: boolean;
Expand All @@ -32,6 +31,7 @@ const LinkWrapper: React.FC<LinkWrapperProps> = memo(
({ href = '', isBeta, onLinkClick, className, currAppId, appId, children, tabIndex, ...props }) => {
const linkRef = useRef<HTMLAnchorElement | null>(null);
const moduleRoutes = useAtomValue(moduleRoutesAtom);
const triggerNavListener = useSetAtom(triggerNavListenersAtom);
const moduleEntry = useMemo(() => moduleRoutes?.find((route) => href?.includes(route.path)), [href, appId]);
const preloadTimeout = useRef<NodeJS.Timeout>();
let actionId = href.split('/').slice(2).join('/');
Expand All @@ -57,7 +57,6 @@ const LinkWrapper: React.FC<LinkWrapperProps> = memo(
*/
type: 'click',
};
const dispatch = useDispatch();
const onClick = (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if (event.ctrlKey || event.shiftKey) {
return false;
Expand All @@ -72,7 +71,7 @@ const LinkWrapper: React.FC<LinkWrapperProps> = memo(
* Add reference to the DOM link element
*/
domEvent.target = linkRef.current;
dispatch(appNavClick({ id: actionId }, domEvent));
triggerNavListener({ navId: actionId, domEvent });
};

// turns /settings/rbac/roles -> settings_rbac_roles
Expand Down
5 changes: 5 additions & 0 deletions src/components/RootApp/ScalprumRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { onRegisterModuleWriteAtom } from '../../state/atoms/chromeModuleAtom';
import useTabName from '../../hooks/useTabName';
import { NotificationData, notificationDrawerDataAtom } from '../../state/atoms/notificationDrawerAtom';
import { isPreviewAtom } from '../../state/atoms/releaseAtom';
import { addNavListenerAtom, deleteNavListenerAtom } from '../../state/atoms/activeAppAtom';

const ProductSelection = lazy(() => import('../Stratosphere/ProductSelection'));

Expand All @@ -59,6 +60,8 @@ const ScalprumRoot = memo(
const registerModule = useSetAtom(onRegisterModuleWriteAtom);
const populateNotifications = useSetAtom(notificationDrawerDataAtom);
const isPreview = useAtomValue(isPreviewAtom);
const addNavListener = useSetAtom(addNavListenerAtom);
const deleteNavListener = useSetAtom(deleteNavListenerAtom);

const store = useStore<ReduxState>();
const mutableChromeApi = useRef<ChromeAPI>();
Expand Down Expand Up @@ -155,6 +158,8 @@ const ScalprumRoot = memo(
chromeAuth,
registerModule,
isPreview,
addNavListener,
deleteNavListener,
});
// reset chrome object after token (user) updates/changes
}, [chromeAuth.token, isPreview]);
Expand Down
75 changes: 49 additions & 26 deletions src/layouts/DefaultLayout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ import DefaultLayout from './DefaultLayout';
import { render } from '@testing-library/react';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { Provider as ProviderJotai } from 'jotai';
import { useHydrateAtoms } from 'jotai/utils';
import { activeAppAtom } from '../state/atoms/activeAppAtom';

const HydrateAtoms = ({ initialValues, children }) => {
useHydrateAtoms(initialValues);
return children;
};

const TestProvider = ({ initialValues, children }) => (
<ProviderJotai>
<HydrateAtoms initialValues={initialValues}>{children}</HydrateAtoms>
</ProviderJotai>
);

jest.mock('../state/atoms/releaseAtom', () => {
const util = jest.requireActual('../state/atoms/utils');
Expand All @@ -28,7 +42,6 @@ describe('DefaultLayout', () => {
mockStore = configureStore();
initialState = {
chrome: {
activeApp: 'some-app',
activeLocation: 'some-location',
appId: 'app-id',
navigation: {
Expand All @@ -51,23 +64,27 @@ describe('DefaultLayout', () => {
it('should render correctly - no data', async () => {
const store = mockStore({ chrome: {} });
const { container } = render(
<Provider store={store}>
<MemoryRouter>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
<TestProvider initialValues={[[activeAppAtom, 'some-app']]}>
<Provider store={store}>
<MemoryRouter>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
</TestProvider>
);
expect(container.querySelector('#chrome-app-render-root')).toMatchSnapshot();
});

it('should render correctly', () => {
const store = mockStore(initialState);
const { container } = render(
<Provider store={store}>
<MemoryRouter initialEntries={['/some-location/app-id']}>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
<TestProvider initialValues={[[activeAppAtom, 'some-app']]}>
<Provider store={store}>
<MemoryRouter initialEntries={['/some-location/app-id']}>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
</TestProvider>
);
expect(container.querySelector('#chrome-app-render-root')).toMatchSnapshot();
});
Expand All @@ -81,11 +98,13 @@ describe('DefaultLayout', () => {
globalFilter: {},
});
const { container } = render(
<Provider store={store}>
<MemoryRouter initialEntries={['/some-location/app-id']}>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
<TestProvider initialValues={[[activeAppAtom, 'some-app']]}>
<Provider store={store}>
<MemoryRouter initialEntries={['/some-location/app-id']}>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
</TestProvider>
);
expect(container.querySelector('#chrome-app-render-root')).toMatchSnapshot();
});
Expand All @@ -98,11 +117,13 @@ describe('DefaultLayout', () => {
},
});
const { container } = render(
<Provider store={store}>
<MemoryRouter initialEntries={['/some-location/app-id']}>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
<TestProvider initialValues={[[activeAppAtom, 'some-app']]}>
<Provider store={store}>
<MemoryRouter initialEntries={['/some-location/app-id']}>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
</TestProvider>
);
expect(container.querySelector('#chrome-app-render-root')).toMatchSnapshot();
});
Expand All @@ -116,11 +137,13 @@ describe('DefaultLayout', () => {
},
});
const { container } = render(
<Provider store={store}>
<MemoryRouter initialEntries={['/some-location/app-id']}>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
<TestProvider initialValues={[[activeAppAtom, 'some-app']]}>
<Provider store={store}>
<MemoryRouter initialEntries={['/some-location/app-id']}>
<DefaultLayout config={config} />
</MemoryRouter>
</Provider>
</TestProvider>
);
expect(container.querySelector('#chrome-app-render-root')).toMatchSnapshot();
});
Expand Down
1 change: 1 addition & 0 deletions src/layouts/__snapshots__/DefaultLayout.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ exports[`DefaultLayout should render correctly - no data 1`] = `
<div
data-ouia-bundle=""
data-ouia-safe="true"
data-ouia-subnav="some-app"
id="chrome-app-render-root"
landing="true"
/>
Expand Down
2 changes: 0 additions & 2 deletions src/redux/action-types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export const USER_LOGIN = '@@chrome/user-login';

export const APP_NAV_CLICK = '@@chrome/app-nav-click';

export const CHROME_PAGE_ACTION = '@@chrome/app-page-action';
export const CHROME_PAGE_OBJECT = '@@chrome/app-object-id';

Expand Down
Loading

0 comments on commit 9b05c69

Please sign in to comment.