diff --git a/.changeset/dry-bananas-ring.md b/.changeset/dry-bananas-ring.md new file mode 100644 index 0000000000..3384879b4a --- /dev/null +++ b/.changeset/dry-bananas-ring.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-notifications-backend': minor +'@backstage/plugin-notifications': minor +--- + +the user can newly mark notifications as "Saved" for their better visibility in the future diff --git a/plugins/notifications-backend/src/service/router.ts b/plugins/notifications-backend/src/service/router.ts index b1425951d8..4c4a756612 100644 --- a/plugins/notifications-backend/src/service/router.ts +++ b/plugins/notifications-backend/src/service/router.ts @@ -224,6 +224,12 @@ export async function createRouter( opts.read = false; // or keep undefined } + if (req.query.saved === 'true') { + opts.saved = true; + } else if (req.query.saved === 'false') { + opts.saved = false; + // or keep undefined + } if (req.query.created_after) { const sinceEpoch = Date.parse(String(req.query.created_after)); if (isNaN(sinceEpoch)) { diff --git a/plugins/notifications/api-report.md b/plugins/notifications/api-report.md index 70ebbd8bfd..be57d9da73 100644 --- a/plugins/notifications/api-report.md +++ b/plugins/notifications/api-report.md @@ -22,6 +22,7 @@ export type GetNotificationsOptions = { limit?: number; search?: string; read?: boolean; + saved?: boolean; createdAfter?: Date; sort?: 'created' | 'topic' | 'origin'; sortOrder?: 'asc' | 'desc'; diff --git a/plugins/notifications/src/api/NotificationsApi.ts b/plugins/notifications/src/api/NotificationsApi.ts index 9820373691..8e80f5908c 100644 --- a/plugins/notifications/src/api/NotificationsApi.ts +++ b/plugins/notifications/src/api/NotificationsApi.ts @@ -30,6 +30,7 @@ export type GetNotificationsOptions = { limit?: number; search?: string; read?: boolean; + saved?: boolean; createdAfter?: Date; sort?: 'created' | 'topic' | 'origin'; sortOrder?: 'asc' | 'desc'; diff --git a/plugins/notifications/src/api/NotificationsClient.ts b/plugins/notifications/src/api/NotificationsClient.ts index 528325a957..d98e0080e8 100644 --- a/plugins/notifications/src/api/NotificationsClient.ts +++ b/plugins/notifications/src/api/NotificationsClient.ts @@ -61,6 +61,9 @@ export class NotificationsClient implements NotificationsApi { if (options?.read !== undefined) { queryString.append('read', options.read ? 'true' : 'false'); } + if (options?.saved !== undefined) { + queryString.append('saved', options.saved ? 'true' : 'false'); + } if (options?.createdAfter !== undefined) { queryString.append('created_after', options.createdAfter.toISOString()); } diff --git a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx index a44a58fe32..ad42c03d16 100644 --- a/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx +++ b/plugins/notifications/src/components/NotificationsFilters/NotificationsFilters.tsx @@ -37,6 +37,8 @@ export type NotificationsFiltersProps = { onCreatedAfterChanged: (value: string) => void; sorting: SortBy; onSortingChanged: (sortBy: SortBy) => void; + saved?: boolean; + onSavedChanged: (checked: boolean | undefined) => void; }; export const CreatedAfterOptions: { @@ -113,6 +115,8 @@ export const NotificationsFilters = ({ onUnreadOnlyChanged, createdAfter, onCreatedAfterChanged, + saved, + onSavedChanged, }: NotificationsFiltersProps) => { const sortByText = getSortByText(sorting); @@ -122,13 +126,23 @@ export const NotificationsFilters = ({ onCreatedAfterChanged(event.target.value as string); }; - const handleOnUnreadOnlyChanged = ( + const handleOnViewChanged = ( event: React.ChangeEvent<{ name?: string; value: unknown }>, ) => { - let value = undefined; - if (event.target.value === 'unread') value = true; - if (event.target.value === 'read') value = false; - onUnreadOnlyChanged(value); + if (event.target.value === 'unread') { + onUnreadOnlyChanged(true); + onSavedChanged(undefined); + } else if (event.target.value === 'read') { + onUnreadOnlyChanged(false); + onSavedChanged(undefined); + } else if (event.target.value === 'saved') { + onUnreadOnlyChanged(undefined); + onSavedChanged(true); + } else { + // All + onUnreadOnlyChanged(undefined); + onSavedChanged(undefined); + } }; const handleOnSortByChanged = ( @@ -139,9 +153,14 @@ export const NotificationsFilters = ({ onSortingChanged({ ...option.sortBy }); }; - let unreadOnlyValue = 'all'; - if (unreadOnly) unreadOnlyValue = 'unread'; - if (unreadOnly === false) unreadOnlyValue = 'read'; + let viewValue = 'all'; + if (saved) { + viewValue = 'saved'; + } else if (unreadOnly) { + viewValue = 'unread'; + } else if (unreadOnly === false) { + viewValue = 'read'; + } return ( <> @@ -156,10 +175,11 @@ export const NotificationsFilters = ({ diff --git a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx index 6b3292b892..54acc5d7bc 100644 --- a/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx +++ b/plugins/notifications/src/components/NotificationsPage/NotificationsPage.tsx @@ -37,6 +37,7 @@ export const NotificationsPage = () => { const [refresh, setRefresh] = React.useState(false); const { lastSignal } = useSignal('notifications'); const [unreadOnly, setUnreadOnly] = React.useState(true); + const [saved, setSaved] = React.useState(undefined); const [pageNumber, setPageNumber] = React.useState(0); const [pageSize, setPageSize] = React.useState(5); const [containsText, setContainsText] = React.useState(); @@ -56,6 +57,9 @@ export const NotificationsPage = () => { if (unreadOnly !== undefined) { options.read = !unreadOnly; } + if (saved !== undefined) { + options.saved = saved; + } const createdAfterDate = CreatedAfterOptions[createdAfter].getDate(); if (createdAfterDate.valueOf() > 0) { @@ -64,7 +68,15 @@ export const NotificationsPage = () => { return api.getNotifications(options); }, - [containsText, unreadOnly, createdAfter, pageNumber, pageSize, sorting], + [ + containsText, + unreadOnly, + createdAfter, + pageNumber, + pageSize, + sorting, + saved, + ], ); useEffect(() => { @@ -100,6 +112,8 @@ export const NotificationsPage = () => { onCreatedAfterChanged={setCreatedAfter} onSortingChanged={setSorting} sorting={sorting} + saved={saved} + onSavedChanged={setSaved} /> diff --git a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx index 3452a618fb..acbeef8769 100644 --- a/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx +++ b/plugins/notifications/src/components/NotificationsTable/NotificationsTable.tsx @@ -17,13 +17,11 @@ import React, { useMemo } from 'react'; import throttle from 'lodash/throttle'; // @ts-ignore import RelativeTime from 'react-relative-time'; -import { Box, IconButton, Tooltip, Typography } from '@material-ui/core'; +import { Box, Grid, IconButton, Tooltip, Typography } from '@material-ui/core'; import { Notification } from '@backstage/plugin-notifications-common'; import { notificationsApiRef } from '../../api'; import { useApi } from '@backstage/core-plugin-api'; -import MarkAsUnreadIcon from '@material-ui/icons/Markunread'; -import MarkAsReadIcon from '@material-ui/icons/CheckCircle'; import { Link, Table, @@ -31,6 +29,11 @@ import { TableColumn, } from '@backstage/core-components'; +import MarkAsUnreadIcon from '@material-ui/icons/Markunread' /* TODO: use Drafts and MarkAsUnread once we have mui 5 icons */; +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'; + const ThrottleDelayMs = 1000; /** @public */ @@ -71,6 +74,18 @@ export const NotificationsTable = ({ [notificationsApi, onUpdate], ); + const onSwitchSavedStatus = React.useCallback( + (notification: Notification) => { + notificationsApi + .updateNotifications({ + ids: [notification.id], + saved: !notification.saved, + }) + .then(() => onUpdate()); + }, + [notificationsApi, onUpdate], + ); + const throttledContainsTextHandler = useMemo( () => throttle(setContainsText, ThrottleDelayMs), [setContainsText], @@ -136,7 +151,6 @@ export const NotificationsTable = ({ // }, // }, { - // TODO: action for saving notifications // actions width: '1rem', render: (notification: Notification) => { @@ -147,24 +161,47 @@ export const NotificationsTable = ({ ? MarkAsUnreadIcon : MarkAsReadIcon; + const markAsSavedText = !!notification.saved + ? 'Undo save' + : 'Save for later'; + + const SavedIconComponent = !!notification.saved + ? MarkAsUnsavedIcon + : MarkAsSavedIcon; + return ( - - { - onSwitchReadStatus(notification); - }} - > - - - + + + + { + onSwitchSavedStatus(notification); + }} + > + + + + + + + + { + onSwitchReadStatus(notification); + }} + > + + + + + ); }, }, ], - [onSwitchReadStatus], + [onSwitchReadStatus, onSwitchSavedStatus], ); - // TODO: render "Saved notifications" as "Pinned" return ( isLoading={isLoading}