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 (