feat: add "Save" action for notifications

Signed-off-by: Marek Libra <marek.libra@gmail.com>
This commit is contained in:
Marek Libra
2024-03-06 10:15:16 +01:00
parent 7ee70ec47e
commit 75f2d84069
8 changed files with 114 additions and 26 deletions
+6
View File
@@ -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
@@ -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)) {
+1
View File
@@ -22,6 +22,7 @@ export type GetNotificationsOptions = {
limit?: number;
search?: string;
read?: boolean;
saved?: boolean;
createdAfter?: Date;
sort?: 'created' | 'topic' | 'origin';
sortOrder?: 'asc' | 'desc';
@@ -30,6 +30,7 @@ export type GetNotificationsOptions = {
limit?: number;
search?: string;
read?: boolean;
saved?: boolean;
createdAfter?: Date;
sort?: 'created' | 'topic' | 'origin';
sortOrder?: 'asc' | 'desc';
@@ -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());
}
@@ -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 = ({
<Select
labelId="notifications-filter-view"
label="View"
value={unreadOnlyValue}
onChange={handleOnUnreadOnlyChanged}
value={viewValue}
onChange={handleOnViewChanged}
>
<MenuItem value="unread">New only</MenuItem>
<MenuItem value="saved">Saved</MenuItem>
<MenuItem value="read">Marked as read</MenuItem>
<MenuItem value="all">All</MenuItem>
</Select>
@@ -37,6 +37,7 @@ export const NotificationsPage = () => {
const [refresh, setRefresh] = React.useState(false);
const { lastSignal } = useSignal('notifications');
const [unreadOnly, setUnreadOnly] = React.useState<boolean | undefined>(true);
const [saved, setSaved] = React.useState<boolean | undefined>(undefined);
const [pageNumber, setPageNumber] = React.useState(0);
const [pageSize, setPageSize] = React.useState(5);
const [containsText, setContainsText] = React.useState<string>();
@@ -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}
/>
</Grid>
<Grid item xs={10}>
@@ -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 (
<Tooltip title={markAsReadText}>
<IconButton
onClick={() => {
onSwitchReadStatus(notification);
}}
>
<IconComponent aria-label={markAsReadText} />
</IconButton>
</Tooltip>
<Grid container wrap="nowrap">
<Grid item>
<Tooltip title={markAsSavedText}>
<IconButton
onClick={() => {
onSwitchSavedStatus(notification);
}}
>
<SavedIconComponent aria-label={markAsSavedText} />
</IconButton>
</Tooltip>
</Grid>
<Grid item>
<Tooltip title={markAsReadText}>
<IconButton
onClick={() => {
onSwitchReadStatus(notification);
}}
>
<IconComponent aria-label={markAsReadText} />
</IconButton>
</Tooltip>
</Grid>
</Grid>
);
},
},
],
[onSwitchReadStatus],
[onSwitchReadStatus, onSwitchSavedStatus],
);
// TODO: render "Saved notifications" as "Pinned"
return (
<Table<Notification>
isLoading={isLoading}