1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00

feat: notifications ui polish (#3232)

This PR adds:
* Keyboard events
* Boxshadow
* Filtering by unread notifications
* Increases smartness for logic around when to prompt whether or not the
functionality is useful
This commit is contained in:
Fredrik Strand Oseberg 2023-03-01 14:28:05 +01:00 committed by GitHub
parent c056c67721
commit b000ecc8fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 107 additions and 45 deletions

View File

@ -16,11 +16,15 @@ const StyledNotificationsIcon = styled(NotificationsIcon)(({ theme }) => ({
marginBottom: theme.spacing(1), marginBottom: theme.spacing(1),
})); }));
export const EmptyNotifications = () => { interface IEmptyNotificationsProps {
text: string;
}
export const EmptyNotifications = ({ text }: IEmptyNotificationsProps) => {
return ( return (
<StyledBox> <StyledBox>
<StyledNotificationsIcon /> <StyledNotificationsIcon />
<Typography color="neutral.main">No new notifications</Typography> <Typography color="neutral.main">{text}</Typography>
</StyledBox> </StyledBox>
); );
}; };

View File

@ -1,5 +1,5 @@
import { useTheme } from '@mui/material'; import { ListItemButton, useTheme } from '@mui/material';
import { Box, ListItem, Typography, styled } from '@mui/material'; import { Box, Typography, styled } from '@mui/material';
import { import {
NotificationsSchemaItem, NotificationsSchemaItem,
NotificationsSchemaItemNotificationType, NotificationsSchemaItemNotificationType,
@ -26,7 +26,7 @@ const StyledContainerBox = styled(Box, {
left: 7, left: 7,
})); }));
const StyledListItem = styled(ListItem)(({ theme }) => ({ const StyledListItemButton = styled(ListItemButton)(({ theme }) => ({
position: 'relative', position: 'relative',
cursor: 'pointer', cursor: 'pointer',
margin: theme.spacing(2, 0), margin: theme.spacing(2, 0),
@ -109,7 +109,7 @@ export const Notification = ({
}; };
return ( return (
<StyledListItem onClick={() => onNotificationClick(notification)}> <StyledListItemButton onClick={() => onNotificationClick(notification)}>
{resolveIcon(notification.notificationType)}{' '} {resolveIcon(notification.notificationType)}{' '}
<StyledNotificationMessageBox> <StyledNotificationMessageBox>
<StyledMessageTypography readAt={Boolean(readAt)}> <StyledMessageTypography readAt={Boolean(readAt)}>
@ -121,6 +121,6 @@ export const Notification = ({
</StyledTimeAgoTypography> </StyledTimeAgoTypography>
</StyledSecondaryInfoBox> </StyledSecondaryInfoBox>
</StyledNotificationMessageBox> </StyledNotificationMessageBox>
</StyledListItem> </StyledListItemButton>
); );
}; };

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { KeyboardEvent, useState } from 'react';
import { import {
Paper, Paper,
Typography, Typography,
@ -7,6 +7,7 @@ import {
styled, styled,
ClickAwayListener, ClickAwayListener,
Button, Button,
Switch,
} from '@mui/material'; } from '@mui/material';
import { useNotifications } from 'hooks/api/getters/useNotifications/useNotifications'; import { useNotifications } from 'hooks/api/getters/useNotifications/useNotifications';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
@ -20,6 +21,7 @@ import { useNavigate } from 'react-router-dom';
import { useNotificationsApi } from 'hooks/api/actions/useNotificationsApi/useNotificationsApi'; import { useNotificationsApi } from 'hooks/api/actions/useNotificationsApi/useNotificationsApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import { flexRow } from 'themes/themeStyles';
import { Feedback } from 'component/common/Feedback/Feedback'; import { Feedback } from 'component/common/Feedback/Feedback';
const StyledPrimaryContainerBox = styled(Box)(() => ({ const StyledPrimaryContainerBox = styled(Box)(() => ({
@ -45,8 +47,10 @@ const StyledPaper = styled(Paper)(({ theme }) => ({
boxShadow: theme.boxShadows.popup, boxShadow: theme.boxShadows.popup,
borderRadius: `${theme.shape.borderRadiusLarge}px`, borderRadius: `${theme.shape.borderRadiusLarge}px`,
position: 'absolute', position: 'absolute',
right: -20, right: -100,
top: 60, top: 60,
maxHeight: '80vh',
overflowY: 'auto',
})); }));
const StyledDotBox = styled(Box)(({ theme }) => ({ const StyledDotBox = styled(Box)(({ theme }) => ({
@ -59,15 +63,24 @@ const StyledDotBox = styled(Box)(({ theme }) => ({
right: 4, right: 4,
})); }));
const StyledHeaderBox = styled(Box)(() => ({
...flexRow,
}));
const StyledHeaderTypography = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
}));
export const Notifications = () => { export const Notifications = () => {
const [showNotifications, setShowNotifications] = useState(false); const [showNotifications, setShowNotifications] = useState(false);
const { notifications, refetchNotifications } = useNotifications({ const { notifications, refetchNotifications } = useNotifications({
refreshInterval: 15, refreshInterval: 15000,
}); });
const navigate = useNavigate(); const navigate = useNavigate();
const { markAsRead } = useNotificationsApi(); const { markAsRead } = useNotificationsApi();
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
const { trackEvent } = usePlausibleTracker(); const { trackEvent } = usePlausibleTracker();
const [showUnread, setShowUnread] = useState(false);
const onNotificationClick = (notification: NotificationsSchemaItem) => { const onNotificationClick = (notification: NotificationsSchemaItem) => {
if (notification.link) { if (notification.link) {
@ -110,6 +123,12 @@ export const Notifications = () => {
} }
}; };
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setShowNotifications(false);
}
};
const unreadNotifications = notifications?.filter( const unreadNotifications = notifications?.filter(
notification => notification.readAt === null notification => notification.readAt === null
); );
@ -118,6 +137,24 @@ export const Notifications = () => {
unreadNotifications && unreadNotifications.length > 0 unreadNotifications && unreadNotifications.length > 0
); );
const filterUnread = (notification: NotificationsSchemaItem) => {
if (showUnread) {
return !Boolean(notification.readAt);
}
return true;
};
const notificationComponents = notifications
?.filter(filterUnread)
.map(notification => (
<Notification
notification={notification}
key={notification.id}
onNotificationClick={onNotificationClick}
/>
));
return ( return (
<StyledPrimaryContainerBox> <StyledPrimaryContainerBox>
<IconButton <IconButton
@ -136,8 +173,19 @@ export const Notifications = () => {
<ClickAwayListener <ClickAwayListener
onClickAway={() => setShowNotifications(false)} onClickAway={() => setShowNotifications(false)}
> >
<StyledPaper> <StyledPaper onKeyDown={onKeyDown}>
<NotificationsHeader /> <NotificationsHeader>
<StyledHeaderBox>
<StyledHeaderTypography>
Show only unread
</StyledHeaderTypography>
<Switch
onClick={() =>
setShowUnread(!showUnread)
}
/>
</StyledHeaderBox>
</NotificationsHeader>
<ConditionallyRender <ConditionallyRender
condition={hasUnreadNotifications} condition={hasUnreadNotifications}
show={ show={
@ -152,26 +200,37 @@ export const Notifications = () => {
} }
/>{' '} />{' '}
<ConditionallyRender <ConditionallyRender
condition={notifications?.length === 0} condition={notificationComponents?.length === 0}
show={<EmptyNotifications />} show={
/> <EmptyNotifications
<NotificationsList> text={
{notifications?.map(notification => ( showUnread
<Notification ? 'No unread notifications'
notification={notification} : 'No new notifications'
key={notification.id}
onNotificationClick={
onNotificationClick
} }
/> />
))} }
/>
<NotificationsList>
{notificationComponents}
</NotificationsList> </NotificationsList>
<ConditionallyRender
condition={Boolean(
notifications &&
notifications.length > 0 &&
!showUnread
)}
show={
<>
<Feedback <Feedback
eventName="notifications" eventName="notifications"
id="useful" id="useful"
localStorageKey="NotificationsUsefulPrompt" localStorageKey="NotificationsUsefulPrompt"
/> />
<br /> <br />
</>
}
/>
</StyledPaper> </StyledPaper>
</ClickAwayListener> </ClickAwayListener>
} }

View File

@ -1,30 +1,29 @@
import Settings from '@mui/icons-material/Settings'; import Settings from '@mui/icons-material/Settings';
import { Typography, Box, IconButton, styled } from '@mui/material'; import { Typography, IconButton, styled, Box } from '@mui/material';
import { flexRow } from 'themes/themeStyles'; import { flexRow } from 'themes/themeStyles';
const StyledOuterContainerBox = styled(Box)(({ theme }) => ({ const StyledOuterContainerBox = styled(Box)(({ theme }) => ({
padding: theme.spacing(1, 3), padding: theme.spacing(1.5, 3, 0.5, 3),
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
})); }));
const StyledSettingsContainer = styled(Box)(() => ({ const StyledInnerBox = styled(Box)(({ theme }) => ({
...flexRow, boxShadow: theme.boxShadows.separator,
width: '100%',
height: '4px',
})); }));
export const NotificationsHeader = () => { export const NotificationsHeader: React.FC = ({ children }) => {
return ( return (
<> <>
<StyledOuterContainerBox> <StyledOuterContainerBox>
<Typography fontWeight="bold">Notifications</Typography> <Typography fontWeight="bold">Notifications</Typography>
{children}
<StyledSettingsContainer>
<IconButton>
<Settings />
</IconButton>
</StyledSettingsContainer>
</StyledOuterContainerBox> </StyledOuterContainerBox>
<StyledInnerBox />
</> </>
); );
}; };

View File

@ -3,10 +3,9 @@ import { Dialogue } from 'component/common/Dialogue/Dialogue';
import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect'; import GeneralSelect from 'component/common/GeneralSelect/GeneralSelect';
import { useExportApi } from 'hooks/api/actions/useExportApi/useExportApi'; import { useExportApi } from 'hooks/api/actions/useExportApi/useExportApi';
import useToast from 'hooks/useToast'; import useToast from 'hooks/useToast';
import { IEnvironment } from 'interfaces/environments';
import { FeatureSchema } from 'openapi'; import { FeatureSchema } from 'openapi';
import { createRef, useState } from 'react'; import { createRef, useEffect, useState } from 'react';
import { formatUnknownError } from 'utils/formatUnknownError'; import { formatUnknownError } from 'utils/formatUnknownError';
interface IExportDialogProps { interface IExportDialogProps {

View File

@ -14,9 +14,7 @@ export const useNotificationsApi = () => {
}); });
try { try {
const res = await makeRequest(req.caller, req.id); await makeRequest(req.caller, req.id);
return res.json();
} catch (e) { } catch (e) {
throw e; throw e;
} }

View File

@ -24,6 +24,7 @@ export default createTheme({
elevated: '0px 1px 20px rgba(45, 42, 89, 0.1)', elevated: '0px 1px 20px rgba(45, 42, 89, 0.1)',
popup: '0px 2px 6px rgba(0, 0, 0, 0.25)', popup: '0px 2px 6px rgba(0, 0, 0, 0.25)',
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)', primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
separator: '0px 2px 3px hsl(0deg 0% 78% / 50%)',
}, },
typography: { typography: {
fontFamily: 'Sen, Roboto, sans-serif', fontFamily: 'Sen, Roboto, sans-serif',

View File

@ -17,6 +17,7 @@ export default createTheme({
elevated: '0px 1px 20px rgba(45, 42, 89, 0.1)', elevated: '0px 1px 20px rgba(45, 42, 89, 0.1)',
popup: '0px 2px 6px rgba(0, 0, 0, 0.25)', popup: '0px 2px 6px rgba(0, 0, 0, 0.25)',
primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)', primaryHeader: '0px 8px 24px rgba(97, 91, 194, 0.2)',
separator: '0px 2px 3px hsl(0deg 0% 78% / 50%)',
}, },
typography: { typography: {
fontFamily: 'Sen, Roboto, sans-serif', fontFamily: 'Sen, Roboto, sans-serif',

View File

@ -28,6 +28,7 @@ declare module '@mui/material/styles' {
elevated: string; elevated: string;
popup: string; popup: string;
primaryHeader: string; primaryHeader: string;
separator: string;
}; };
} }