feat(notifications): added Mark All Read bulk action

The user can newly mark all unread messages as read at one click.

Signed-off-by: Marek Libra <marek.libra@gmail.com>
This commit is contained in:
Marek Libra
2024-04-23 10:28:40 +02:00
parent bd900b7e0e
commit f730c0b54f
5 changed files with 121 additions and 37 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-notifications': patch
---
The user can newly mark all unread messages as read at one click.
+2
View File
@@ -112,6 +112,7 @@ export const NotificationsSidebarItem: (props?: {
export const NotificationsTable: ({
isLoading,
notifications,
isUnread,
onUpdate,
setContainsText,
onPageChange,
@@ -127,6 +128,7 @@ export type NotificationsTableProps = Pick<
'onPageChange' | 'onRowsPerPageChange' | 'page' | 'totalCount'
> & {
isLoading?: boolean;
isUnread: boolean;
notifications?: Notification_2[];
onUpdate: () => void;
setContainsText: (search: string) => void;
@@ -22,6 +22,7 @@ import {
ResponseErrorPanel,
} from '@backstage/core-components';
import Grid from '@material-ui/core/Grid';
import { ConfirmProvider } from 'material-ui-confirm';
import { useSignal } from '@backstage/plugin-signals-react';
import { NotificationsTable } from '../NotificationsTable';
@@ -32,8 +33,11 @@ import {
SortBy,
SortByOptions,
} from '../NotificationsFilters';
import { GetNotificationsOptions } from '../../api';
import { NotificationSeverity } from '@backstage/plugin-notifications-common';
import { GetNotificationsOptions, GetNotificationsResponse } from '../../api';
import {
NotificationSeverity,
NotificationStatus,
} from '@backstage/plugin-notifications-common';
const ThrottleDelayMs = 2000;
@@ -70,7 +74,9 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
);
const [severity, setSeverity] = React.useState<NotificationSeverity>('low');
const { error, value, retry, loading } = useNotificationsApi(
const { error, value, retry, loading } = useNotificationsApi<
[GetNotificationsResponse, NotificationStatus]
>(
api => {
const options: GetNotificationsOptions = {
search: containsText,
@@ -91,7 +97,7 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
options.createdAfter = createdAfterDate;
}
return api.getNotifications(options);
return Promise.all([api.getNotifications(options), api.getStatus()]);
},
[
containsText,
@@ -131,6 +137,10 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
return <ResponseErrorPanel error={error} />;
}
const notifications = value?.[0]?.notifications;
const totalCount = value?.[0]?.totalCount;
const isUnread = !!value?.[1]?.unread;
return (
<PageWithHeader
title={title}
@@ -141,35 +151,38 @@ export const NotificationsPage = (props?: NotificationsPageProps) => {
typeLink={typeLink}
>
<Content>
<Grid container>
<Grid item xs={2}>
<NotificationsFilters
unreadOnly={unreadOnly}
onUnreadOnlyChanged={setUnreadOnly}
createdAfter={createdAfter}
onCreatedAfterChanged={setCreatedAfter}
onSortingChanged={setSorting}
sorting={sorting}
saved={saved}
onSavedChanged={setSaved}
severity={severity}
onSeverityChanged={setSeverity}
/>
<ConfirmProvider>
<Grid container>
<Grid item xs={2}>
<NotificationsFilters
unreadOnly={unreadOnly}
onUnreadOnlyChanged={setUnreadOnly}
createdAfter={createdAfter}
onCreatedAfterChanged={setCreatedAfter}
onSortingChanged={setSorting}
sorting={sorting}
saved={saved}
onSavedChanged={setSaved}
severity={severity}
onSeverityChanged={setSeverity}
/>
</Grid>
<Grid item xs={10}>
<NotificationsTable
isLoading={loading}
isUnread={isUnread}
notifications={notifications}
onUpdate={onUpdate}
setContainsText={setContainsText}
onPageChange={setPageNumber}
onRowsPerPageChange={setPageSize}
page={pageNumber}
pageSize={pageSize}
totalCount={totalCount}
/>
</Grid>
</Grid>
<Grid item xs={10}>
<NotificationsTable
isLoading={loading}
notifications={value?.notifications}
onUpdate={onUpdate}
setContainsText={setContainsText}
onPageChange={setPageNumber}
onRowsPerPageChange={setPageSize}
page={pageNumber}
pageSize={pageSize}
totalCount={value?.totalCount}
/>
</Grid>
</Grid>
</ConfirmProvider>
</Content>
</PageWithHeader>
);
@@ -22,17 +22,22 @@ import MarkAsUnreadIcon from '@material-ui/icons/Markunread' /* TODO: use Drafts
import MarkAsReadIcon from '@material-ui/icons/CheckCircle';
import MarkAsUnsavedIcon from '@material-ui/icons/LabelOff' /* TODO: use BookmarkRemove and BookmarkAdd once we have mui 5 icons */;
import MarkAsSavedIcon from '@material-ui/icons/Label';
import MarkAllReadIcon from '@material-ui/icons/DoneAll';
export const BulkActions = ({
selectedNotifications,
notifications,
isUnread,
onSwitchReadStatus,
onSwitchSavedStatus,
onMarkAllRead,
}: {
selectedNotifications: Set<Notification['id']>;
notifications: Notification[];
isUnread?: boolean;
onSwitchReadStatus: (ids: Notification['id'][], newStatus: boolean) => void;
onSwitchSavedStatus: (ids: Notification['id'][], newStatus: boolean) => void;
onMarkAllRead?: () => void;
}) => {
const isDisabled = selectedNotifications.size === 0;
const bulkNotifications = notifications.filter(notification =>
@@ -58,7 +63,22 @@ export const BulkActions = ({
return (
<Grid container wrap="nowrap">
<Grid item>
<Grid item xs={3}>
{onMarkAllRead ? (
<Tooltip title="Mark all read">
<div>
{/* The <div> here is a workaround for the Tooltip which does not work for a "disabled" child */}
<IconButton disabled={!isUnread} onClick={onMarkAllRead}>
<MarkAllReadIcon aria-label={markAsSavedText} />
</IconButton>
</div>
</Tooltip>
) : (
<div />
)}
</Grid>
<Grid item xs={3}>
<Tooltip title={markAsSavedText}>
<div>
{/* The <div> here is a workaround for the Tooltip which does not work for a "disabled" child */}
@@ -74,7 +94,7 @@ export const BulkActions = ({
</Tooltip>
</Grid>
<Grid item>
<Grid item xs={3}>
<Tooltip title={markAsReadText}>
<div>
<IconButton
@@ -23,9 +23,8 @@ import CheckBox from '@material-ui/core/Checkbox';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import { Notification } from '@backstage/plugin-notifications-common';
import { notificationsApiRef } from '../../api';
import { useApi } from '@backstage/core-plugin-api';
import { useConfirm } from 'material-ui-confirm';
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
import {
Link,
Table,
@@ -33,6 +32,8 @@ import {
TableColumn,
} from '@backstage/core-components';
import { notificationsApiRef } from '../../api';
import { SeverityIcon } from './SeverityIcon';
import { SelectAll } from './SelectAll';
import { BulkActions } from './BulkActions';
@@ -55,6 +56,7 @@ export type NotificationsTableProps = Pick<
'onPageChange' | 'onRowsPerPageChange' | 'page' | 'totalCount'
> & {
isLoading?: boolean;
isUnread: boolean;
notifications?: Notification[];
onUpdate: () => void;
setContainsText: (search: string) => void;
@@ -65,6 +67,7 @@ export type NotificationsTableProps = Pick<
export const NotificationsTable = ({
isLoading,
notifications = [],
isUnread,
onUpdate,
setContainsText,
onPageChange,
@@ -75,6 +78,9 @@ export const NotificationsTable = ({
}: NotificationsTableProps) => {
const classes = useStyles();
const notificationsApi = useApi(notificationsApiRef);
const alertApi = useApi(alertApiRef);
const confirm = useConfirm();
const [selectedNotifications, setSelectedNotifications] = React.useState(
new Set<Notification['id']>(),
);
@@ -117,6 +123,39 @@ export const NotificationsTable = ({
[notificationsApi, onUpdate],
);
const onMarkAllRead = React.useCallback(() => {
confirm({
title: 'Are you sure?',
description: (
<>
Mark <b>all</b> notifications as <b>read</b>.
</>
),
confirmationText: 'Mark All',
})
.then(async () => {
const ids = (
await notificationsApi.getNotifications({ read: false })
).notifications?.map(notification => notification.id);
return notificationsApi
.updateNotifications({
ids,
read: true,
})
.then(onUpdate);
})
.catch(e => {
if (e) {
// if e === undefined, the Cancel button has been hit
alertApi.post({
message: 'Failed to mark all notifications as read',
severity: 'error',
});
}
});
}, [alertApi, confirm, notificationsApi, onUpdate]);
const throttledContainsTextHandler = React.useMemo(
() => throttle(setContainsText, ThrottleDelayMs),
[setContainsText],
@@ -210,8 +249,10 @@ export const NotificationsTable = ({
<BulkActions
notifications={notifications}
selectedNotifications={selectedNotifications}
isUnread={isUnread}
onSwitchReadStatus={onSwitchReadStatus}
onSwitchSavedStatus={onSwitchSavedStatus}
onMarkAllRead={onMarkAllRead}
/>
),
render: (notification: Notification) => (
@@ -220,6 +261,7 @@ export const NotificationsTable = ({
selectedNotifications={new Set([notification.id])}
onSwitchReadStatus={onSwitchReadStatus}
onSwitchSavedStatus={onSwitchSavedStatus}
//
/>
),
},
@@ -227,8 +269,10 @@ export const NotificationsTable = ({
[
selectedNotifications,
notifications,
isUnread,
onSwitchReadStatus,
onSwitchSavedStatus,
onMarkAllRead,
onNotificationsSelectChange,
classes.severityItem,
classes.description,