From f730c0b54f167ee5ecc231fcec445c601661c26a Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Tue, 23 Apr 2024 10:28:40 +0200 Subject: [PATCH 1/2] 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 --- .changeset/funny-bees-taste.md | 5 ++ plugins/notifications/api-report.md | 2 + .../NotificationsPage/NotificationsPage.tsx | 77 +++++++++++-------- .../NotificationsTable/BulkActions.tsx | 24 +++++- .../NotificationsTable/NotificationsTable.tsx | 50 +++++++++++- 5 files changed, 121 insertions(+), 37 deletions(-) create mode 100644 .changeset/funny-bees-taste.md 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, From 23b461f0f0eb4f84e670bad991d9ff4eae4375f5 Mon Sep 17 00:00:00 2001 From: Marek Libra Date: Tue, 23 Apr 2024 10:31:06 +0200 Subject: [PATCH 2/2] Add material-ui-confirm dependency Signed-off-by: Marek Libra --- plugins/notifications/package.json | 1 + yarn.lock | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/plugins/notifications/package.json b/plugins/notifications/package.json index 9832871023..9da48caefc 100644 --- a/plugins/notifications/package.json +++ b/plugins/notifications/package.json @@ -43,6 +43,7 @@ "@material-ui/lab": "^4.0.0-alpha.61", "@types/react": "^16.13.1 || ^17.0.0", "lodash": "^4.17.21", + "material-ui-confirm": "^3.0.12", "notistack": "^3.0.1", "react-relative-time": "^0.0.9", "react-use": "^17.2.4" diff --git a/yarn.lock b/yarn.lock index 9a940a725a..bcb3acdc0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6212,6 +6212,7 @@ __metadata: "@testing-library/user-event": ^14.0.0 "@types/react": ^16.13.1 || ^17.0.0 lodash: ^4.17.21 + material-ui-confirm: ^3.0.12 msw: ^1.0.0 notistack: ^3.0.1 react-relative-time: ^0.0.9 @@ -30548,6 +30549,17 @@ __metadata: languageName: node linkType: hard +"material-ui-confirm@npm:^3.0.12": + version: 3.0.12 + resolution: "material-ui-confirm@npm:3.0.12" + peerDependencies: + "@mui/material": ">= 5.0.0" + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + checksum: de1092fdc4ab8104fbfdb5034921aeb2a5db5854ad5e31fb24dc4c204c6fb5b66a33ad377abf7adda390c432aefe96a2188ef1b8a3b9c1a80a9a28af63833458 + languageName: node + linkType: hard + "material-ui-popup-state@npm:^1.9.3": version: 1.9.3 resolution: "material-ui-popup-state@npm:1.9.3"