diff --git a/.changeset/funny-bees-taste.md b/.changeset/funny-bees-taste.md new file mode 100644 index 0000000000..a9b2d84997 --- /dev/null +++ b/.changeset/funny-bees-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-notifications': patch +--- + +The user can newly mark all unread messages as read at one click. diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 9ffcf0d727..8fbd148b6d 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -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; diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 539db4f61e..f1dd9aba5f 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -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('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 ; } + const notifications = value?.[0]?.notifications; + const totalCount = value?.[0]?.totalCount; + const isUnread = !!value?.[1]?.unread; + return ( { typeLink={typeLink} > - - - + + + + + + + + - - - - + ); diff --git a/plugins/notifications/src/components/NotificationsTable/BulkActions.tsx b/plugins/notifications/src/components/NotificationsTable/BulkActions.tsx index 034da7c93c..40e2f5c8bb 100644 --- a/plugins/notifications/src/components/NotificationsTable/BulkActions.tsx +++ b/plugins/notifications/src/components/NotificationsTable/BulkActions.tsx @@ -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; 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 ( - + + {onMarkAllRead ? ( + +
+ {/* The
here is a workaround for the Tooltip which does not work for a "disabled" child */} + + + +
+ + ) : ( +
+ )} + + +
{/* The
here is a workaround for the Tooltip which does not work for a "disabled" child */} @@ -74,7 +94,7 @@ export const BulkActions = ({ - +
& { 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(), ); @@ -117,6 +123,39 @@ export const NotificationsTable = ({ [notificationsApi, onUpdate], ); + const onMarkAllRead = React.useCallback(() => { + confirm({ + title: 'Are you sure?', + description: ( + <> + Mark all notifications as read. + + ), + 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 = ({ ), 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,