feat: support saving and marking notifications done
Signed-off-by: Heikki Hellgren <heikki.hellgren@op.fi>
This commit is contained in:
@@ -22,13 +22,17 @@ export default async function createPlugin(
|
||||
env: PluginEnvironment,
|
||||
): Promise<Router> {
|
||||
// TODO: Remove this test code
|
||||
let notifications = 0;
|
||||
setInterval(() => {
|
||||
env.notificationService.send(
|
||||
'user:default/guest',
|
||||
'Test',
|
||||
'This is test notification',
|
||||
'/catalog',
|
||||
);
|
||||
if (notifications < 10) {
|
||||
env.notificationService.send({
|
||||
entityRef: 'user:default/guest',
|
||||
title: 'Test',
|
||||
description: 'This is test notification',
|
||||
link: '/catalog',
|
||||
});
|
||||
notifications++;
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
return await createRouter({
|
||||
|
||||
@@ -70,7 +70,49 @@ export async function createRouter(
|
||||
res.send(status);
|
||||
});
|
||||
|
||||
// TODO: Add endpoint to set read/unread by notification id(s)
|
||||
router.post('/read', async (req, res) => {
|
||||
const user = await getUser(req);
|
||||
const { ids } = req.body;
|
||||
if (!ids || !Array.isArray(ids)) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
await store.markRead({ user_ref: user, ids });
|
||||
res.status(200).send({ ids });
|
||||
});
|
||||
|
||||
router.post('/unread', async (req, res) => {
|
||||
const user = await getUser(req);
|
||||
const { ids } = req.body;
|
||||
if (!ids || !Array.isArray(ids)) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
await store.markUnread({ user_ref: user, ids });
|
||||
res.status(200).send({ ids });
|
||||
});
|
||||
|
||||
router.post('/save', async (req, res) => {
|
||||
const user = await getUser(req);
|
||||
const { ids } = req.body;
|
||||
if (!ids || !Array.isArray(ids)) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
await store.markSaved({ user_ref: user, ids });
|
||||
res.status(200).send({ ids });
|
||||
});
|
||||
|
||||
router.post('/unsave', async (req, res) => {
|
||||
const user = await getUser(req);
|
||||
const { ids } = req.body;
|
||||
if (!ids || !Array.isArray(ids)) {
|
||||
res.status(400).send();
|
||||
return;
|
||||
}
|
||||
await store.markUnsaved({ user_ref: user, ids });
|
||||
res.status(200).send({ ids });
|
||||
});
|
||||
|
||||
router.use(errorHandler());
|
||||
return router;
|
||||
|
||||
@@ -11,11 +11,18 @@ type Notification_2 = {
|
||||
description: string;
|
||||
link: string;
|
||||
icon?: string;
|
||||
image?: string;
|
||||
created: Date;
|
||||
read?: Date;
|
||||
saved: boolean;
|
||||
};
|
||||
export { Notification_2 as Notification };
|
||||
|
||||
// @public (undocumented)
|
||||
export type NotificationIds = {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type NotificationStatus = {
|
||||
unread: number;
|
||||
|
||||
@@ -26,8 +26,10 @@ export type Notification = {
|
||||
link: string;
|
||||
// TODO: Icon should be typed so that we know what to render
|
||||
icon?: string;
|
||||
image?: string;
|
||||
created: Date;
|
||||
read?: Date;
|
||||
saved: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
@@ -35,3 +37,8 @@ export type NotificationStatus = {
|
||||
unread: number;
|
||||
read: number;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type NotificationIds = {
|
||||
ids: string[];
|
||||
};
|
||||
|
||||
@@ -28,6 +28,14 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
read: number;
|
||||
}>;
|
||||
// (undocumented)
|
||||
markRead(options: NotificationModifyOptions): Promise<void>;
|
||||
// (undocumented)
|
||||
markSaved(options: NotificationModifyOptions): Promise<void>;
|
||||
// (undocumented)
|
||||
markUnread(options: NotificationModifyOptions): Promise<void>;
|
||||
// (undocumented)
|
||||
markUnsaved(options: NotificationModifyOptions): Promise<void>;
|
||||
// (undocumented)
|
||||
saveNotification(notification: Notification_2): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -37,6 +45,21 @@ export type NotificationGetOptions = {
|
||||
type?: NotificationType;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type NotificationModifyOptions = {
|
||||
ids: string[];
|
||||
} & NotificationGetOptions;
|
||||
|
||||
// @public (undocumented)
|
||||
export type NotificationSendOptions = {
|
||||
entityRef: string | string[];
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
image?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export class NotificationService {
|
||||
// (undocumented)
|
||||
@@ -47,12 +70,7 @@ export class NotificationService {
|
||||
// (undocumented)
|
||||
getStore(): Promise<NotificationsStore>;
|
||||
// (undocumented)
|
||||
send(
|
||||
entityRef: string | string[],
|
||||
title: string,
|
||||
description: string,
|
||||
link: string,
|
||||
): Promise<Notification_2[]>;
|
||||
send(options: NotificationSendOptions): Promise<Notification_2[]>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -71,6 +89,14 @@ export interface NotificationsStore {
|
||||
// (undocumented)
|
||||
getStatus(options: NotificationGetOptions): Promise<NotificationStatus>;
|
||||
// (undocumented)
|
||||
markRead(options: NotificationModifyOptions): Promise<void>;
|
||||
// (undocumented)
|
||||
markSaved(options: NotificationModifyOptions): Promise<void>;
|
||||
// (undocumented)
|
||||
markUnread(options: NotificationModifyOptions): Promise<void>;
|
||||
// (undocumented)
|
||||
markUnsaved(options: NotificationModifyOptions): Promise<void>;
|
||||
// (undocumented)
|
||||
saveNotification(notification: Notification_2): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -21,8 +21,11 @@ exports.up = async function up(knex) {
|
||||
table.string('title').notNullable();
|
||||
table.text('description').notNullable();
|
||||
table.text('link').notNullable();
|
||||
table.text('icon').nullable();
|
||||
table.text('image').nullable();
|
||||
table.datetime('created').notNullable();
|
||||
table.datetime('read').nullable();
|
||||
table.boolean('saved').defaultTo(false).notNullable();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '@backstage/backend-common';
|
||||
import {
|
||||
NotificationGetOptions,
|
||||
NotificationModifyOptions,
|
||||
NotificationsStore,
|
||||
} from './NotificationsStore';
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
@@ -55,7 +56,9 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
return typeof val === 'string' ? Number.parseInt(val, 10) : val ?? 0;
|
||||
};
|
||||
|
||||
private getNotificationsBaseQuery = (options: NotificationGetOptions) => {
|
||||
private getNotificationsBaseQuery = (
|
||||
options: NotificationGetOptions | NotificationModifyOptions,
|
||||
) => {
|
||||
const { user_ref, type } = options;
|
||||
const query = this.db('notifications').where('userRef', user_ref);
|
||||
|
||||
@@ -63,8 +66,14 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
query.whereNull('read');
|
||||
} else if (type === 'read') {
|
||||
query.whereNotNull('read');
|
||||
} else if (type === 'saved') {
|
||||
query.where('saved', true);
|
||||
}
|
||||
// TODO: Saved
|
||||
|
||||
if ('ids' in options && options.ids) {
|
||||
query.whereIn('id', options.ids);
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
@@ -96,4 +105,24 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
read: this.mapToInteger((readQuery as any)?.READ),
|
||||
};
|
||||
}
|
||||
|
||||
async markRead(options: NotificationModifyOptions): Promise<void> {
|
||||
const notificationQuery = this.getNotificationsBaseQuery(options);
|
||||
await notificationQuery.update({ read: new Date() });
|
||||
}
|
||||
|
||||
async markUnread(options: NotificationModifyOptions): Promise<void> {
|
||||
const notificationQuery = this.getNotificationsBaseQuery(options);
|
||||
await notificationQuery.update({ read: null });
|
||||
}
|
||||
|
||||
async markSaved(options: NotificationModifyOptions): Promise<void> {
|
||||
const notificationQuery = this.getNotificationsBaseQuery(options);
|
||||
await notificationQuery.update({ saved: true });
|
||||
}
|
||||
|
||||
async markUnsaved(options: NotificationModifyOptions): Promise<void> {
|
||||
const notificationQuery = this.getNotificationsBaseQuery(options);
|
||||
await notificationQuery.update({ saved: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ export type NotificationGetOptions = {
|
||||
type?: NotificationType;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type NotificationModifyOptions = {
|
||||
ids: string[];
|
||||
} & NotificationGetOptions;
|
||||
|
||||
/** @public */
|
||||
export interface NotificationsStore {
|
||||
getNotifications(options: NotificationGetOptions): Promise<Notification[]>;
|
||||
@@ -34,5 +39,11 @@ export interface NotificationsStore {
|
||||
|
||||
getStatus(options: NotificationGetOptions): Promise<NotificationStatus>;
|
||||
|
||||
// TODO: Mark as read/unread by notification id(s)
|
||||
markRead(options: NotificationModifyOptions): Promise<void>;
|
||||
|
||||
markUnread(options: NotificationModifyOptions): Promise<void>;
|
||||
|
||||
markSaved(options: NotificationModifyOptions): Promise<void>;
|
||||
|
||||
markUnsaved(options: NotificationModifyOptions): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,16 @@ export type NotificationServiceOptions = {
|
||||
discovery: PluginEndpointDiscovery;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type NotificationSendOptions = {
|
||||
entityRef: string | string[];
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
image?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export class NotificationService {
|
||||
private store: NotificationsStore | null = null;
|
||||
@@ -56,12 +66,8 @@ export class NotificationService {
|
||||
return new NotificationService(database, catalogClient);
|
||||
}
|
||||
|
||||
async send(
|
||||
entityRef: string | string[],
|
||||
title: string,
|
||||
description: string,
|
||||
link: string,
|
||||
): Promise<Notification[]> {
|
||||
async send(options: NotificationSendOptions): Promise<Notification[]> {
|
||||
const { entityRef, title, description, link, icon, image } = options;
|
||||
const users = await this.getUsersForEntityRef(entityRef);
|
||||
const notifications = [];
|
||||
const store = await this.getStore();
|
||||
@@ -73,6 +79,9 @@ export class NotificationService {
|
||||
description,
|
||||
link,
|
||||
created: new Date(),
|
||||
icon,
|
||||
image,
|
||||
saved: false,
|
||||
};
|
||||
|
||||
await store.saveNotification(notification);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { FetchApi } from '@backstage/core-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { Notification as Notification_2 } from '@backstage/plugin-notifications-common';
|
||||
import { NotificationIds } from '@backstage/plugin-notifications-common';
|
||||
import { NotificationStatus } from '@backstage/plugin-notifications-common';
|
||||
import { NotificationType } from '@backstage/plugin-notifications-common';
|
||||
import { default as React_2 } from 'react';
|
||||
@@ -29,6 +30,14 @@ export interface NotificationsApi {
|
||||
): Promise<Notification_2[]>;
|
||||
// (undocumented)
|
||||
getStatus(): Promise<NotificationStatus>;
|
||||
// (undocumented)
|
||||
markRead(ids: string[]): Promise<NotificationIds>;
|
||||
// (undocumented)
|
||||
markSaved(ids: string[]): Promise<NotificationIds>;
|
||||
// (undocumented)
|
||||
markUnread(ids: string[]): Promise<NotificationIds>;
|
||||
// (undocumented)
|
||||
markUnsaved(ids: string[]): Promise<NotificationIds>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -43,6 +52,14 @@ export class NotificationsClient implements NotificationsApi {
|
||||
): Promise<Notification_2[]>;
|
||||
// (undocumented)
|
||||
getStatus(): Promise<NotificationStatus>;
|
||||
// (undocumented)
|
||||
markRead(ids: string[]): Promise<NotificationIds>;
|
||||
// (undocumented)
|
||||
markSaved(ids: string[]): Promise<NotificationIds>;
|
||||
// (undocumented)
|
||||
markUnread(ids: string[]): Promise<NotificationIds>;
|
||||
// (undocumented)
|
||||
markUnsaved(ids: string[]): Promise<NotificationIds>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -61,6 +78,9 @@ export const NotificationsSidebarItem: () => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const NotificationsTable: (props: {
|
||||
onUpdate: () => void;
|
||||
type: NotificationType;
|
||||
loading?: boolean;
|
||||
notifications?: Notification_2[];
|
||||
}) => React_2.JSX.Element;
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"@material-ui/core": "^4.9.13",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"react-relative-time": "^0.0.9",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
Notification,
|
||||
NotificationIds,
|
||||
NotificationStatus,
|
||||
NotificationType,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
@@ -36,6 +37,11 @@ export interface NotificationsApi {
|
||||
|
||||
getStatus(): Promise<NotificationStatus>;
|
||||
|
||||
// TODO: Mark as read/unread by notification id(s)
|
||||
// TODO: Mark as saved/unsaved by notification id(s)
|
||||
markRead(ids: string[]): Promise<NotificationIds>;
|
||||
|
||||
markUnread(ids: string[]): Promise<NotificationIds>;
|
||||
|
||||
markSaved(ids: string[]): Promise<NotificationIds>;
|
||||
|
||||
markUnsaved(ids: string[]): Promise<NotificationIds>;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import {
|
||||
Notification,
|
||||
NotificationIds,
|
||||
NotificationStatus,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
|
||||
@@ -44,18 +45,50 @@ export class NotificationsClient implements NotificationsApi {
|
||||
|
||||
const urlSegment = `notifications?${queryString}`;
|
||||
|
||||
return await this.get<Notification[]>(urlSegment);
|
||||
return await this.request<Notification[]>(urlSegment);
|
||||
}
|
||||
|
||||
async getStatus(): Promise<NotificationStatus> {
|
||||
return await this.get<NotificationStatus>('status');
|
||||
return await this.request<NotificationStatus>('status');
|
||||
}
|
||||
|
||||
private async get<T>(path: string): Promise<T> {
|
||||
async markRead(ids: string[]): Promise<NotificationIds> {
|
||||
return await this.request<NotificationIds>('read', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: ids }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
async markUnread(ids: string[]): Promise<NotificationIds> {
|
||||
return await this.request<NotificationIds>('unread', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: ids }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
async markSaved(ids: string[]): Promise<NotificationIds> {
|
||||
return await this.request<NotificationIds>('save', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: ids }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
async markUnsaved(ids: string[]): Promise<NotificationIds> {
|
||||
return await this.request<NotificationIds>('unsave', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ ids: ids }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
private async request<T>(path: string, init?: any): Promise<T> {
|
||||
const baseUrl = `${await this.discoveryApi.getBaseUrl('notifications')}/`;
|
||||
const url = new URL(path, baseUrl);
|
||||
|
||||
const response = await this.fetchApi.fetch(url.toString());
|
||||
const response = await this.fetchApi.fetch(url.toString(), init);
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
|
||||
@@ -44,12 +44,14 @@ const useStyles = makeStyles(_theme => ({
|
||||
export const NotificationsPage = () => {
|
||||
const [type, setType] = useState<NotificationType>('unread');
|
||||
|
||||
const {
|
||||
loading: _loading,
|
||||
error,
|
||||
value,
|
||||
retry: _retry,
|
||||
} = useNotificationsApi(api => api.getNotifications({ type }), [type]);
|
||||
const { loading, error, value, retry } = useNotificationsApi(
|
||||
api => api.getNotifications({ type }),
|
||||
[type],
|
||||
);
|
||||
|
||||
const onUpdate = () => {
|
||||
retry();
|
||||
};
|
||||
|
||||
const styles = useStyles();
|
||||
if (error) {
|
||||
@@ -89,7 +91,12 @@ export const NotificationsPage = () => {
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<TableContainer component={Paper}>
|
||||
<NotificationsTable notifications={value} />
|
||||
<NotificationsTable
|
||||
notifications={value}
|
||||
type={type}
|
||||
loading={loading}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
</TableContainer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -13,8 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Table,
|
||||
@@ -24,20 +26,45 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import { Notification } from '@backstage/plugin-notifications-common';
|
||||
import {
|
||||
Notification,
|
||||
NotificationType,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import NotificationsIcon from '@material-ui/icons/Notifications';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import Check from '@material-ui/icons/Check';
|
||||
import Bookmark from '@material-ui/icons/Bookmark';
|
||||
import { notificationsApiRef } from '../../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import Inbox from '@material-ui/icons/Inbox';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import { Skeleton } from '@material-ui/lab';
|
||||
// @ts-ignore
|
||||
import RelativeTime from 'react-relative-time';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
notificationRow: {
|
||||
cursor: 'pointer',
|
||||
'&.hideOnHover': {
|
||||
display: 'initial',
|
||||
},
|
||||
'& .showOnHover': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.linkHover,
|
||||
'& .hideOnHover': {
|
||||
display: 'none',
|
||||
},
|
||||
'& .showOnHover': {
|
||||
display: 'initial',
|
||||
},
|
||||
},
|
||||
},
|
||||
actionButton: {
|
||||
padding: '9px',
|
||||
},
|
||||
checkBox: {
|
||||
padding: '0 10px 10px 0',
|
||||
},
|
||||
@@ -45,25 +72,102 @@ const useStyles = makeStyles(theme => ({
|
||||
|
||||
/** @public */
|
||||
export const NotificationsTable = (props: {
|
||||
onUpdate: () => void;
|
||||
type: NotificationType;
|
||||
loading?: boolean;
|
||||
notifications?: Notification[];
|
||||
}) => {
|
||||
const { notifications } = props;
|
||||
const { notifications, type, loading } = props;
|
||||
const navigate = useNavigate();
|
||||
const styles = useStyles();
|
||||
// TODO: Add select all
|
||||
// TODO: Make mark as read work
|
||||
// TODO: Check status of the notification and change to "Mark as unread" if it's already read
|
||||
// TODO: Add support to save notifications (storageApi)
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const notificationsApi = useApi(notificationsApiRef);
|
||||
|
||||
const onCheckBoxClick = (id: string) => {
|
||||
const index = selected.indexOf(id);
|
||||
if (index !== -1) {
|
||||
setSelected(selected.filter(s => s !== id));
|
||||
} else {
|
||||
setSelected([...selected, id]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelected([]);
|
||||
}, [type]);
|
||||
|
||||
const isChecked = (id: string) => {
|
||||
return selected.indexOf(id) !== -1;
|
||||
};
|
||||
|
||||
const isAllSelected = () => {
|
||||
return (
|
||||
selected.length === notifications?.length && notifications.length > 0
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton variant="rect" height={200} />;
|
||||
}
|
||||
|
||||
// TODO: Show timestamp relative time (react-relative-time npm package)
|
||||
// TODO: Add signals listener and refresh data on message
|
||||
// TODO: Handle no notifications properly
|
||||
// TODO: Handle loading notifications
|
||||
return (
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3}>
|
||||
{notifications?.length ?? 0} notifications
|
||||
{type !== 'saved' && !notifications?.length && 'No notifications'}
|
||||
{type !== 'saved' && !!notifications?.length && (
|
||||
<Checkbox
|
||||
size="small"
|
||||
style={{ paddingLeft: 0 }}
|
||||
checked={isAllSelected()}
|
||||
onClick={() => {
|
||||
if (isAllSelected()) {
|
||||
setSelected([]);
|
||||
} else {
|
||||
setSelected(
|
||||
notifications ? notifications.map(n => n.id) : [],
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === 'saved' &&
|
||||
`${notifications?.length ?? 0} saved notifications`}
|
||||
{selected.length === 0 &&
|
||||
!!notifications?.length &&
|
||||
type !== 'saved' &&
|
||||
'Select all'}
|
||||
{selected.length > 0 && `${selected.length} selected`}
|
||||
{type === 'read' && selected.length > 0 && (
|
||||
<Button
|
||||
startIcon={<Inbox fontSize="small" />}
|
||||
onClick={() => {
|
||||
notificationsApi
|
||||
.markUnread(selected)
|
||||
.then(() => props.onUpdate());
|
||||
setSelected([]);
|
||||
}}
|
||||
>
|
||||
Move to inbox
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{type === 'unread' && selected.length > 0 && (
|
||||
<Button
|
||||
startIcon={<Check fontSize="small" />}
|
||||
onClick={() => {
|
||||
notificationsApi
|
||||
.markRead(selected)
|
||||
.then(() => props.onUpdate());
|
||||
setSelected([]);
|
||||
}}
|
||||
>
|
||||
Mark as done
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -71,7 +175,12 @@ export const NotificationsTable = (props: {
|
||||
return (
|
||||
<TableRow key={notification.id} className={styles.notificationRow}>
|
||||
<TableCell width={100} style={{ verticalAlign: 'center' }}>
|
||||
<Checkbox className={styles.checkBox} size="small" />
|
||||
<Checkbox
|
||||
className={styles.checkBox}
|
||||
size="small"
|
||||
checked={isChecked(notification.id)}
|
||||
onClick={() => onCheckBoxClick(notification.id)}
|
||||
/>
|
||||
{notification.icon ?? <NotificationsIcon fontSize="small" />}
|
||||
</TableCell>
|
||||
<TableCell onClick={() => navigate(notification.link)}>
|
||||
@@ -81,16 +190,67 @@ export const NotificationsTable = (props: {
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'right' }}>
|
||||
<Tooltip title="Mark as read">
|
||||
<IconButton>
|
||||
<Check fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Save">
|
||||
<IconButton>
|
||||
<Bookmark fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Box className="hideOnHover">
|
||||
<RelativeTime value={notification.created} />
|
||||
</Box>
|
||||
<Box className="showOnHover">
|
||||
<Tooltip
|
||||
title={notification.read ? 'Move to inbox' : 'Mark as done'}
|
||||
>
|
||||
<IconButton
|
||||
className={styles.actionButton}
|
||||
onClick={() => {
|
||||
if (notification.read) {
|
||||
notificationsApi
|
||||
.markUnread([notification.id])
|
||||
.then(() => {
|
||||
props.onUpdate();
|
||||
});
|
||||
} else {
|
||||
notificationsApi
|
||||
.markRead([notification.id])
|
||||
.then(() => {
|
||||
props.onUpdate();
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{notification.read ? (
|
||||
<Inbox fontSize="small" />
|
||||
) : (
|
||||
<Check fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={notification.saved ? 'Remove from saved' : 'Save'}
|
||||
>
|
||||
<IconButton
|
||||
className={styles.actionButton}
|
||||
onClick={() => {
|
||||
if (notification.saved) {
|
||||
notificationsApi
|
||||
.markUnsaved([notification.id])
|
||||
.then(() => {
|
||||
props.onUpdate();
|
||||
});
|
||||
} else {
|
||||
notificationsApi
|
||||
.markSaved([notification.id])
|
||||
.then(() => {
|
||||
props.onUpdate();
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{notification.saved ? (
|
||||
<CloseIcon fontSize="small" />
|
||||
) : (
|
||||
<Bookmark fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -7845,6 +7845,7 @@ __metadata:
|
||||
"@testing-library/react": ^12.1.3
|
||||
"@testing-library/user-event": ^14.0.0
|
||||
msw: ^1.0.0
|
||||
react-relative-time: ^0.0.9
|
||||
react-use: ^17.2.4
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0
|
||||
@@ -39162,6 +39163,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-relative-time@npm:^0.0.9":
|
||||
version: 0.0.9
|
||||
resolution: "react-relative-time@npm:0.0.9"
|
||||
peerDependencies:
|
||||
react: ">=0.13.0"
|
||||
checksum: 9c25887125df0eccfd4fee1edc4c99b19bef262ed5c66ac93269b63f0e1654a7c2758489d0d4b534847decc542dac23555896a7e2a0492af4c0ffbf48f1c62fe
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-remove-scroll-bar@npm:^2.3.3":
|
||||
version: 2.3.4
|
||||
resolution: "react-remove-scroll-bar@npm:2.3.4"
|
||||
|
||||
Reference in New Issue
Block a user