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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-notifications': patch
|
||||
---
|
||||
|
||||
The user can newly mark all unread messages as read at one click.
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user