fleet/frontend/context/notification.tsx
jacobshandling d3ccb51755
UI - Improve UX of Flash messages (#22836)
## #22661 


![ezgif-6-71e48912ae](https://github.com/user-attachments/assets/01144620-0eba-48f0-9254-cc4795fde9fd)

- Update `FlashMessage` behavior to, by default, hide itself when the
user performs any URL-changing navigation
- Add `persistOnPageChange` option to `renderFlash` API and associated
notification context and reducer logic, allowing override of this
behavior on a per-call basis
- Ensure proper order of evaluation of URL changes and render flash
action dispatches on the event loop
- Clean up legacy unused "undo"-related arguments and logic
- Allow the user to click in the same horizontal dimension as a flash
message
- Other misc. cleanup and refactoring

[Demo - messages hidden on page (any URL)
change](https://www.loom.com/share/1e884b6ba11c4b59bc74f51df3690131?sid=9b53e78b-6535-4541-b676-377760366cf4)

- [x] Changes file added for user-visible changes in `changes/`,
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2024-10-22 10:52:20 -07:00

106 lines
2.4 KiB
TypeScript

import React, {
createContext,
useReducer,
ReactNode,
useCallback,
useMemo,
} from "react";
import { INotification } from "interfaces/notification";
import { noop } from "lodash";
type Props = {
children: ReactNode;
};
type InitialStateType = {
notification: INotification | null;
renderFlash: (
alertType: "success" | "error" | "warning-filled" | null,
message: JSX.Element | string | null,
options?: { persistOnPageChange?: boolean }
) => void;
hideFlash: () => void;
};
export type INotificationContext = InitialStateType;
const initialState = {
notification: null,
renderFlash: noop,
hideFlash: noop,
};
const actionTypes = {
RENDER_FLASH: "RENDER_FLASH",
HIDE_FLASH: "HIDE_FLASH",
} as const;
const reducer = (state: any, action: any) => {
switch (action.type) {
case actionTypes.RENDER_FLASH:
return {
...state,
notification: {
alertType: action.alertType,
isVisible: true,
message: action.message,
persistOnPageChange: action.options?.persistOnPageChange ?? false,
},
};
case actionTypes.HIDE_FLASH:
return initialState;
default:
return state;
}
};
export const NotificationContext = createContext<InitialStateType>(
initialState
);
const NotificationProvider = ({ children }: Props) => {
const [state, dispatch] = useReducer(reducer, initialState);
const renderFlash = useCallback(
(
alertType: "success" | "error" | "warning-filled" | null,
message: JSX.Element | string | null,
options?: {
persistOnPageChange?: boolean;
}
) => {
// wrapping the dispatch in a timeout ensures it is evaluated on the next event loop,
// preventing bugs related to the FlashMessage's self-hiding behavior on URL changes.
// react router v3 router.push is asynchronous
setTimeout(() => {
dispatch({
type: actionTypes.RENDER_FLASH,
alertType,
message,
options,
});
});
},
[]
);
const hideFlash = useCallback(() => {
dispatch({ type: actionTypes.HIDE_FLASH });
}, []);
const value = useMemo(
() => ({
notification: state.notification,
renderFlash,
hideFlash,
}),
[state.notification, renderFlash, hideFlash]
);
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
);
};
export default NotificationProvider;