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),
}));
export const EmptyNotifications = () => {
interface IEmptyNotificationsProps {
text: string;
}
export const EmptyNotifications = ({ text }: IEmptyNotificationsProps) => {
return (
<StyledBox>
<StyledNotificationsIcon />
<Typography color="neutral.main">No new notifications</Typography>
<Typography color="neutral.main">{text}</Typography>
</StyledBox>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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