feat: update Notifications front-end
Signed-off-by: Marek Libra <marek.libra@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-notifications-backend': minor
|
||||
'@backstage/plugin-notifications': minor
|
||||
'@backstage/plugin-notifications-common': patch
|
||||
---
|
||||
|
||||
The Notifications frontend has been redesigned towards list view with condensed row details. The 'done' attribute has been removed to keep the Notifications aligned with the idea of a messaging system instead of a task manager.
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
exports.up = async function up(knex) {
|
||||
await knex.schema.alterTable('notification', table => {
|
||||
table.text('link').nullable().alter();
|
||||
table.dropColumn('done');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = async function down(knex) {
|
||||
await knex.schema.alterTable('notification', table => {
|
||||
table.text('link').notNullable().alter();
|
||||
table.datetime('done').nullable();
|
||||
});
|
||||
};
|
||||
@@ -62,7 +62,6 @@ describe.each(databases.eachSupportedId())(
|
||||
const insertNotification = async (
|
||||
notification: Partial<Notification> & {
|
||||
id: string;
|
||||
done?: Date;
|
||||
saved?: Date;
|
||||
read?: Date;
|
||||
},
|
||||
@@ -78,7 +77,6 @@ describe.each(databases.eachSupportedId())(
|
||||
title: notification.payload?.title,
|
||||
severity: notification.payload?.severity,
|
||||
scope: notification.payload?.scope,
|
||||
done: notification.done,
|
||||
saved: notification.saved,
|
||||
read: notification.read,
|
||||
})
|
||||
@@ -104,44 +102,68 @@ describe.each(databases.eachSupportedId())(
|
||||
|
||||
const notifications = await storage.getNotifications({ user });
|
||||
expect(notifications.length).toBe(2);
|
||||
expect(notifications.find(el => el.id === id1)).toBeTruthy();
|
||||
expect(notifications.find(el => el.id === id2)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return undone notifications for user', async () => {
|
||||
it('should return read notifications for user', async () => {
|
||||
const id1 = uuid();
|
||||
const id2 = uuid();
|
||||
await insertNotification({
|
||||
id: id1,
|
||||
...testNotification,
|
||||
done: new Date(),
|
||||
});
|
||||
const id3 = uuid();
|
||||
await insertNotification({ id: id1, ...testNotification });
|
||||
await insertNotification({ id: id2, ...testNotification });
|
||||
await insertNotification({ id: id3, ...testNotification });
|
||||
await insertNotification({ id: uuid(), ...otherUserNotification });
|
||||
|
||||
await storage.markRead({ ids: [id1, id3], user });
|
||||
|
||||
const notifications = await storage.getNotifications({
|
||||
user,
|
||||
type: 'undone',
|
||||
read: true,
|
||||
});
|
||||
expect(notifications.length).toBe(2);
|
||||
expect(notifications.find(el => el.id === id1)).toBeTruthy();
|
||||
expect(notifications.find(el => el.id === id3)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return unread notifications for user', async () => {
|
||||
const id1 = uuid();
|
||||
const id2 = uuid();
|
||||
const id3 = uuid();
|
||||
await insertNotification({ id: id1, ...testNotification });
|
||||
await insertNotification({ id: id2, ...testNotification });
|
||||
await insertNotification({ id: id3, ...testNotification });
|
||||
await insertNotification({ id: uuid(), ...otherUserNotification });
|
||||
|
||||
await storage.markRead({ ids: [id1, id3], user });
|
||||
|
||||
const notifications = await storage.getNotifications({
|
||||
user,
|
||||
read: false,
|
||||
});
|
||||
expect(notifications.length).toBe(1);
|
||||
expect(notifications.at(0)?.id).toEqual(id2);
|
||||
});
|
||||
|
||||
it('should return done notifications for user', async () => {
|
||||
it('should return both read and unread notifications for user', async () => {
|
||||
const id1 = uuid();
|
||||
const id2 = uuid();
|
||||
await insertNotification({
|
||||
id: id1,
|
||||
...testNotification,
|
||||
done: new Date(),
|
||||
});
|
||||
const id3 = uuid();
|
||||
await insertNotification({ id: id1, ...testNotification });
|
||||
await insertNotification({ id: id2, ...testNotification });
|
||||
await insertNotification({ id: id3, ...testNotification });
|
||||
await insertNotification({ id: uuid(), ...otherUserNotification });
|
||||
|
||||
await storage.markRead({ ids: [id1, id3], user });
|
||||
|
||||
const notifications = await storage.getNotifications({
|
||||
user,
|
||||
type: 'done',
|
||||
read: undefined,
|
||||
});
|
||||
expect(notifications.length).toBe(1);
|
||||
expect(notifications.at(0)?.id).toEqual(id1);
|
||||
expect(notifications.length).toBe(3);
|
||||
expect(notifications.find(el => el.id === id1)).toBeTruthy();
|
||||
expect(notifications.find(el => el.id === id2)).toBeTruthy();
|
||||
expect(notifications.find(el => el.id === id3)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should allow searching for notifications', async () => {
|
||||
@@ -218,7 +240,6 @@ describe.each(databases.eachSupportedId())(
|
||||
...testNotification,
|
||||
id: id1,
|
||||
read: new Date(),
|
||||
done: new Date(),
|
||||
payload: {
|
||||
title: 'Notification',
|
||||
link: '/scaffolder/task/1234',
|
||||
@@ -242,7 +263,6 @@ describe.each(databases.eachSupportedId())(
|
||||
expect(existing).not.toBeNull();
|
||||
expect(existing?.id).toEqual(id1);
|
||||
expect(existing?.payload.title).toEqual('New notification');
|
||||
expect(existing?.done).toBeNull();
|
||||
expect(existing?.read).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -283,32 +303,6 @@ describe.each(databases.eachSupportedId())(
|
||||
});
|
||||
});
|
||||
|
||||
describe('markDone', () => {
|
||||
it('should mark notification done', async () => {
|
||||
const id1 = uuid();
|
||||
await insertNotification({ id: id1, ...testNotification });
|
||||
|
||||
await storage.markDone({ ids: [id1], user });
|
||||
const notification = await storage.getNotification({ id: id1 });
|
||||
expect(notification?.done).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('markUndone', () => {
|
||||
it('should mark notification undone', async () => {
|
||||
const id1 = uuid();
|
||||
await insertNotification({
|
||||
id: id1,
|
||||
...testNotification,
|
||||
done: new Date(),
|
||||
});
|
||||
|
||||
await storage.markUndone({ ids: [id1], user });
|
||||
const notification = await storage.getNotification({ id: id1 });
|
||||
expect(notification?.done).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('markSaved', () => {
|
||||
it('should mark notification saved', async () => {
|
||||
const id1 = uuid();
|
||||
@@ -334,5 +328,26 @@ describe.each(databases.eachSupportedId())(
|
||||
expect(notification?.saved).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveNotification', () => {
|
||||
it('should store a notification', async () => {
|
||||
const id1 = uuid();
|
||||
await storage.saveNotification({
|
||||
id: id1,
|
||||
user,
|
||||
created: new Date(),
|
||||
origin: 'my-origin',
|
||||
payload: {
|
||||
title: 'My title One',
|
||||
description: 'a description of the notification',
|
||||
link: 'http://foo.bar',
|
||||
severity: 'normal',
|
||||
topic: 'my-topic',
|
||||
},
|
||||
});
|
||||
const notification = await storage.getNotification({ id: id1 });
|
||||
expect(notification?.payload?.title).toBe('My title One');
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -61,7 +61,6 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
id: row.id,
|
||||
user: row.user,
|
||||
created: row.created,
|
||||
done: row.done,
|
||||
saved: row.saved,
|
||||
read: row.read,
|
||||
updated: row.updated,
|
||||
@@ -78,10 +77,27 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
}));
|
||||
};
|
||||
|
||||
private mapNotificationToDbRow = (notification: Notification) => {
|
||||
return {
|
||||
id: notification.id,
|
||||
user: notification.user,
|
||||
origin: notification.origin,
|
||||
created: notification.created,
|
||||
topic: notification.payload?.topic,
|
||||
link: notification.payload?.link,
|
||||
title: notification.payload?.title,
|
||||
description: notification.payload?.description,
|
||||
severity: notification.payload?.severity,
|
||||
scope: notification.payload?.scope,
|
||||
saved: notification.saved,
|
||||
read: notification.read,
|
||||
};
|
||||
};
|
||||
|
||||
private getNotificationsBaseQuery = (
|
||||
options: NotificationGetOptions | NotificationModifyOptions,
|
||||
) => {
|
||||
const { user, type } = options;
|
||||
const { user } = options;
|
||||
const query = this.db('notification').where('user', user);
|
||||
|
||||
if (options.sort !== undefined && options.sort !== null) {
|
||||
@@ -90,14 +106,6 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
query.orderBy('created', options.sortOrder ?? 'desc');
|
||||
}
|
||||
|
||||
if (type === 'undone') {
|
||||
query.whereNull('done');
|
||||
} else if (type === 'done') {
|
||||
query.whereNotNull('done');
|
||||
} else if (type === 'saved') {
|
||||
query.whereNotNull('saved');
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query.limit(options.limit);
|
||||
}
|
||||
@@ -117,6 +125,18 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
query.whereIn('notification.id', options.ids);
|
||||
}
|
||||
|
||||
if (options.read) {
|
||||
query.whereNotNull('notification.read');
|
||||
} else if (options.read === false) {
|
||||
query.whereNull('notification.read');
|
||||
} // or match both if undefined
|
||||
|
||||
if (options.saved) {
|
||||
query.whereNotNull('notification.saved');
|
||||
} else if (options.saved === false) {
|
||||
query.whereNull('notification.saved');
|
||||
} // or match both if undefined
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
@@ -127,7 +147,9 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
}
|
||||
|
||||
async saveNotification(notification: Notification) {
|
||||
await this.db.insert(notification).into('notification');
|
||||
await this.db
|
||||
.insert(this.mapNotificationToDbRow(notification))
|
||||
.into('notification');
|
||||
}
|
||||
|
||||
async getStatus(options: NotificationGetOptions) {
|
||||
@@ -188,10 +210,9 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
description: options.notification.payload.description,
|
||||
link: options.notification.payload.link,
|
||||
topic: options.notification.payload.topic,
|
||||
updated: options.notification.created,
|
||||
updated: new Date(),
|
||||
severity: options.notification.payload.severity,
|
||||
read: null,
|
||||
done: null,
|
||||
});
|
||||
|
||||
return await this.getNotification(options);
|
||||
@@ -218,16 +239,6 @@ export class DatabaseNotificationsStore implements NotificationsStore {
|
||||
await notificationQuery.update({ read: null });
|
||||
}
|
||||
|
||||
async markDone(options: NotificationModifyOptions): Promise<void> {
|
||||
const notificationQuery = this.getNotificationsBaseQuery(options);
|
||||
await notificationQuery.update({ done: new Date(), read: new Date() });
|
||||
}
|
||||
|
||||
async markUndone(options: NotificationModifyOptions): Promise<void> {
|
||||
const notificationQuery = this.getNotificationsBaseQuery(options);
|
||||
await notificationQuery.update({ done: null, read: null });
|
||||
}
|
||||
|
||||
async markSaved(options: NotificationModifyOptions): Promise<void> {
|
||||
const notificationQuery = this.getNotificationsBaseQuery(options);
|
||||
await notificationQuery.update({ saved: new Date() });
|
||||
|
||||
@@ -17,19 +17,20 @@
|
||||
import {
|
||||
Notification,
|
||||
NotificationStatus,
|
||||
NotificationType,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
|
||||
// TODO: reuse the common part of the type with front-end
|
||||
/** @internal */
|
||||
export type NotificationGetOptions = {
|
||||
user: string;
|
||||
ids?: string[];
|
||||
type?: NotificationType;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
sort?: 'created' | 'read' | 'updated' | null;
|
||||
sortOrder?: 'asc' | 'desc';
|
||||
read?: boolean;
|
||||
saved?: boolean;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
@@ -62,10 +63,6 @@ export interface NotificationsStore {
|
||||
|
||||
markUnread(options: NotificationModifyOptions): Promise<void>;
|
||||
|
||||
markDone(options: NotificationModifyOptions): Promise<void>;
|
||||
|
||||
markUndone(options: NotificationModifyOptions): Promise<void>;
|
||||
|
||||
markSaved(options: NotificationModifyOptions): Promise<void>;
|
||||
|
||||
markUnsaved(options: NotificationModifyOptions): Promise<void>;
|
||||
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
NewNotificationSignal,
|
||||
Notification,
|
||||
NotificationReadSignal,
|
||||
NotificationType,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
|
||||
/** @internal */
|
||||
@@ -190,9 +189,6 @@ export async function createRouter(
|
||||
const opts: NotificationGetOptions = {
|
||||
user: user,
|
||||
};
|
||||
if (req.query.type) {
|
||||
opts.type = req.query.type.toString() as NotificationType;
|
||||
}
|
||||
if (req.query.offset) {
|
||||
opts.offset = Number.parseInt(req.query.offset.toString(), 10);
|
||||
}
|
||||
@@ -202,6 +198,12 @@ export async function createRouter(
|
||||
if (req.query.search) {
|
||||
opts.search = req.query.search.toString();
|
||||
}
|
||||
if (req.query.read === 'true') {
|
||||
opts.read = true;
|
||||
} else if (req.query.read === 'false') {
|
||||
opts.read = false;
|
||||
// or keep undefined
|
||||
}
|
||||
|
||||
const notifications = await store.getNotifications(opts);
|
||||
res.send(notifications);
|
||||
@@ -224,7 +226,7 @@ export async function createRouter(
|
||||
|
||||
router.get('/status', async (req, res) => {
|
||||
const user = await getUser(req);
|
||||
const status = await store.getStatus({ user, type: 'undone' });
|
||||
const status = await store.getStatus({ user });
|
||||
res.send(status);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ type Notification_2 = {
|
||||
created: Date;
|
||||
saved?: Date;
|
||||
read?: Date;
|
||||
done?: Date;
|
||||
updated?: Date;
|
||||
origin: string;
|
||||
payload: NotificationPayload;
|
||||
@@ -27,7 +26,7 @@ export { Notification_2 as Notification };
|
||||
export type NotificationPayload = {
|
||||
title: string;
|
||||
description?: string;
|
||||
link: string;
|
||||
link?: string;
|
||||
severity: NotificationSeverity;
|
||||
topic?: string;
|
||||
scope?: string;
|
||||
@@ -51,7 +50,4 @@ export type NotificationStatus = {
|
||||
unread: number;
|
||||
read: number;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type NotificationType = 'undone' | 'done' | 'saved';
|
||||
```
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/** @public */
|
||||
export type NotificationType = 'undone' | 'done' | 'saved';
|
||||
|
||||
/** @public */
|
||||
export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low';
|
||||
|
||||
@@ -24,7 +21,7 @@ export type NotificationSeverity = 'critical' | 'high' | 'normal' | 'low';
|
||||
export type NotificationPayload = {
|
||||
title: string;
|
||||
description?: string;
|
||||
link: string;
|
||||
link?: string;
|
||||
// TODO: Add support for additional links
|
||||
// additionalLinks?: string[];
|
||||
severity: NotificationSeverity;
|
||||
@@ -40,7 +37,6 @@ export type Notification = {
|
||||
created: Date;
|
||||
saved?: Date;
|
||||
read?: Date;
|
||||
done?: Date;
|
||||
updated?: Date;
|
||||
origin: string;
|
||||
payload: NotificationPayload;
|
||||
|
||||
@@ -12,16 +12,15 @@ 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 { NotificationStatus } from '@backstage/plugin-notifications-common';
|
||||
import { NotificationType } from '@backstage/plugin-notifications-common';
|
||||
import { default as React_2 } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export type GetNotificationsOptions = {
|
||||
type?: NotificationType;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
read?: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -78,16 +77,24 @@ export const NotificationsSidebarItem: (props?: {
|
||||
}) => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export const NotificationsTable: (props: {
|
||||
onUpdate: () => void;
|
||||
type: NotificationType;
|
||||
export const NotificationsTable: ({
|
||||
isLoading,
|
||||
notifications,
|
||||
onUpdate,
|
||||
setContainsText,
|
||||
}: NotificationsTableProps) => React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export type NotificationsTableProps = {
|
||||
isLoading?: boolean;
|
||||
notifications?: Notification_2[];
|
||||
}) => React_2.JSX.Element;
|
||||
onUpdate: () => void;
|
||||
setContainsText: (search: string) => void;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type UpdateNotificationsOptions = {
|
||||
ids: string[];
|
||||
done?: boolean;
|
||||
read?: boolean;
|
||||
saved?: boolean;
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"react-relative-time": "^0.0.9",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
|
||||
@@ -17,7 +17,6 @@ import { createApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
Notification,
|
||||
NotificationStatus,
|
||||
NotificationType,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
|
||||
/** @public */
|
||||
@@ -27,16 +26,15 @@ export const notificationsApiRef = createApiRef<NotificationsApi>({
|
||||
|
||||
/** @public */
|
||||
export type GetNotificationsOptions = {
|
||||
type?: NotificationType;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
read?: boolean;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type UpdateNotificationsOptions = {
|
||||
ids: string[];
|
||||
done?: boolean;
|
||||
read?: boolean;
|
||||
saved?: boolean;
|
||||
};
|
||||
|
||||
@@ -60,16 +60,16 @@ describe('NotificationsClient', () => {
|
||||
server.use(
|
||||
rest.get(`${mockBaseUrl}/`, (req, res, ctx) => {
|
||||
expect(req.url.search).toBe(
|
||||
'?type=undone&limit=10&offset=0&search=find+me',
|
||||
'?limit=10&offset=0&search=find+me&read=true',
|
||||
);
|
||||
return res(ctx.json(expectedResp));
|
||||
}),
|
||||
);
|
||||
const response = await client.getNotifications({
|
||||
type: 'undone',
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
search: 'find me',
|
||||
read: true,
|
||||
});
|
||||
expect(response).toEqual(expectedResp);
|
||||
});
|
||||
@@ -103,14 +103,12 @@ describe('NotificationsClient', () => {
|
||||
rest.post(`${mockBaseUrl}/update`, async (req, res, ctx) => {
|
||||
expect(await req.json()).toEqual({
|
||||
ids: ['acdaa8ca-262b-43c1-b74b-de06e5f3b3c7'],
|
||||
done: true,
|
||||
});
|
||||
return res(ctx.json(expectedResp));
|
||||
}),
|
||||
);
|
||||
const response = await client.updateNotifications({
|
||||
ids: ['acdaa8ca-262b-43c1-b74b-de06e5f3b3c7'],
|
||||
done: true,
|
||||
});
|
||||
expect(response).toEqual(expectedResp);
|
||||
});
|
||||
|
||||
@@ -42,9 +42,6 @@ export class NotificationsClient implements NotificationsApi {
|
||||
options?: GetNotificationsOptions,
|
||||
): Promise<Notification[]> {
|
||||
const queryString = new URLSearchParams();
|
||||
if (options?.type) {
|
||||
queryString.append('type', options.type);
|
||||
}
|
||||
if (options?.limit !== undefined) {
|
||||
queryString.append('limit', options.limit.toString(10));
|
||||
}
|
||||
@@ -54,6 +51,9 @@ export class NotificationsClient implements NotificationsApi {
|
||||
if (options?.search) {
|
||||
queryString.append('search', options.search);
|
||||
}
|
||||
if (options?.read !== undefined) {
|
||||
queryString.append('read', options.read ? 'true' : 'false');
|
||||
}
|
||||
|
||||
const urlSegment = `?${queryString}`;
|
||||
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Divider,
|
||||
FormControl,
|
||||
Grid,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
|
||||
export type NotificationsFiltersProps = {
|
||||
unreadOnly?: boolean;
|
||||
onUnreadOnlyChanged: (checked: boolean | undefined) => void;
|
||||
// createdAfter?: string;
|
||||
// sorting?: {
|
||||
// orderBy: GetNotificationsOrderByEnum;
|
||||
// orderByDirec: GetNotificationsOrderByDirecEnum;
|
||||
// };
|
||||
// onCreatedAfterChanged: (value: string) => void;
|
||||
// setSorting: ({
|
||||
// orderBy,
|
||||
// orderByDirec,
|
||||
// }: {
|
||||
// orderBy: GetNotificationsOrderByEnum;
|
||||
// orderByDirec: GetNotificationsOrderByDirecEnum;
|
||||
// }) => void;
|
||||
};
|
||||
|
||||
// export const CreatedAfterOptions: {
|
||||
// [key: string]: { label: string; getDate: () => Date };
|
||||
// } = {
|
||||
// last24h: {
|
||||
// label: 'Last 24h',
|
||||
// getDate: () => new Date(Date.now() - 24 * 3600 * 1000),
|
||||
// },
|
||||
// lastWeek: {
|
||||
// label: 'Last week',
|
||||
// getDate: () => new Date(Date.now() - 7 * 24 * 3600 * 1000),
|
||||
// },
|
||||
// all: {
|
||||
// label: 'Any time',
|
||||
// getDate: () => new Date(0),
|
||||
// },
|
||||
// };
|
||||
|
||||
// export const SortByOptions: {
|
||||
// [key: string]: {
|
||||
// label: string;
|
||||
// orderBy: GetNotificationsOrderByEnum;
|
||||
// orderByDirec: GetNotificationsOrderByDirecEnum;
|
||||
// };
|
||||
// } = {
|
||||
// newest: {
|
||||
// label: 'Newest on top',
|
||||
// orderBy: GetNotificationsOrderByEnum.Created,
|
||||
// orderByDirec: GetNotificationsOrderByDirecEnum.Asc,
|
||||
// },
|
||||
// oldest: {
|
||||
// label: 'Oldest on top',
|
||||
// orderBy: GetNotificationsOrderByEnum.Created,
|
||||
// orderByDirec: GetNotificationsOrderByDirecEnum.Desc,
|
||||
// },
|
||||
// topic: {
|
||||
// label: 'Topic',
|
||||
// orderBy: GetNotificationsOrderByEnum.Topic,
|
||||
// orderByDirec: GetNotificationsOrderByDirecEnum.Asc,
|
||||
// },
|
||||
// origin: {
|
||||
// label: 'Origin',
|
||||
// orderBy: GetNotificationsOrderByEnum.Origin,
|
||||
// orderByDirec: GetNotificationsOrderByDirecEnum.Asc,
|
||||
// },
|
||||
// };
|
||||
|
||||
// TODO: Implement sorting on server (to work with pagination)
|
||||
// const getSortBy = (sorting: NotificationsFiltersProps['sorting']): string => {
|
||||
// if (
|
||||
// sorting?.orderBy === GetNotificationsOrderByEnum.Created &&
|
||||
// sorting.orderByDirec === GetNotificationsOrderByDirecEnum.Desc
|
||||
// ) {
|
||||
// return 'oldest';
|
||||
// }
|
||||
// if (sorting?.orderBy === GetNotificationsOrderByEnum.Topic) {
|
||||
// return 'topic';
|
||||
// }
|
||||
// if (sorting?.orderBy === GetNotificationsOrderByEnum.Origin) {
|
||||
// return 'origin';
|
||||
// }
|
||||
|
||||
// return 'newest';
|
||||
// };
|
||||
|
||||
export const NotificationsFilters = ({
|
||||
unreadOnly,
|
||||
// createdAfter,
|
||||
// sorting,
|
||||
// onCreatedAfterChanged,
|
||||
onUnreadOnlyChanged,
|
||||
}: // setSorting,
|
||||
NotificationsFiltersProps) => {
|
||||
// const sortBy = getSortBy(sorting);
|
||||
|
||||
// const handleOnCreatedAfterChanged = (
|
||||
// event: React.ChangeEvent<{ name?: string; value: unknown }>,
|
||||
// ) => {
|
||||
// onCreatedAfterChanged(event.target.value as string);
|
||||
// };
|
||||
|
||||
const handleOnUnreadOnlyChanged = (
|
||||
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);
|
||||
};
|
||||
|
||||
// const handleOnSortByChanged = (
|
||||
// event: React.ChangeEvent<{ name?: string; value: unknown }>,
|
||||
// ) => {
|
||||
// const idx = (event.target.value as string) || 'newest';
|
||||
// const option = SortByOptions[idx];
|
||||
// setSorting({
|
||||
// orderBy: option.orderBy,
|
||||
// orderByDirec: option.orderByDirec,
|
||||
// });
|
||||
// };
|
||||
|
||||
let unreadOnlyValue = 'all';
|
||||
if (unreadOnly) unreadOnlyValue = 'unread';
|
||||
if (unreadOnly === false) unreadOnlyValue = 'read';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Filters</Typography>
|
||||
<Divider variant="fullWidth" />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-view">View</InputLabel>
|
||||
<Select
|
||||
labelId="notifications-filter-view"
|
||||
label="View"
|
||||
value={unreadOnlyValue}
|
||||
onChange={handleOnUnreadOnlyChanged}
|
||||
>
|
||||
<MenuItem value="unread">New only</MenuItem>
|
||||
<MenuItem value="read">Marked as read</MenuItem>
|
||||
<MenuItem value="all">All</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
{/* TODO: extend BE to support following:
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-view">
|
||||
Created after
|
||||
</InputLabel>
|
||||
|
||||
<Select
|
||||
label="Created after"
|
||||
placeholder="Notifications since"
|
||||
value={createdAfter}
|
||||
onChange={handleOnCreatedAfterChanged}
|
||||
>
|
||||
{Object.keys(CreatedAfterOptions).map((key: string) => (
|
||||
<MenuItem value={key} key={key}>
|
||||
{CreatedAfterOptions[key].label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormControl fullWidth variant="outlined" size="small">
|
||||
<InputLabel id="notifications-filter-sort">Sort by</InputLabel>
|
||||
|
||||
<Select
|
||||
label="Sort by"
|
||||
placeholder="Field to sort by"
|
||||
value={sortBy}
|
||||
onChange={handleOnSortByChanged}
|
||||
>
|
||||
{Object.keys(SortByOptions).map((key: string) => (
|
||||
<MenuItem value={key} key={key}>
|
||||
{SortByOptions[key].label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid> */}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2024 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './NotificationsFilters';
|
||||
@@ -14,35 +14,35 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Content,
|
||||
ErrorPanel,
|
||||
PageWithHeader,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { NotificationsTable } from '../NotificationsTable';
|
||||
import { useNotificationsApi } from '../../hooks';
|
||||
import { Button, Grid, makeStyles } from '@material-ui/core';
|
||||
import Bookmark from '@material-ui/icons/Bookmark';
|
||||
import Check from '@material-ui/icons/Check';
|
||||
import Inbox from '@material-ui/icons/Inbox';
|
||||
import { NotificationType } from '@backstage/plugin-notifications-common';
|
||||
import { Grid } from '@material-ui/core';
|
||||
import { useSignal } from '@backstage/plugin-signals-react';
|
||||
|
||||
const useStyles = makeStyles(_theme => ({
|
||||
filterButton: {
|
||||
width: '100%',
|
||||
justifyContent: 'start',
|
||||
},
|
||||
}));
|
||||
import { NotificationsFilters } from '../NotificationsFilters';
|
||||
import { GetNotificationsOptions } from '../../api';
|
||||
|
||||
export const NotificationsPage = () => {
|
||||
const [type, setType] = useState<NotificationType>('undone');
|
||||
const [refresh, setRefresh] = React.useState(false);
|
||||
const { lastSignal } = useSignal('notifications');
|
||||
const [unreadOnly, setUnreadOnly] = React.useState<boolean | undefined>(true);
|
||||
const [containsText, setContainsText] = React.useState<string>();
|
||||
|
||||
const { error, value, retry } = useNotificationsApi(
|
||||
api => api.getNotifications({ type }),
|
||||
[type],
|
||||
const { error, value, retry, loading } = useNotificationsApi(
|
||||
// TODO: add pagination and other filters
|
||||
api => {
|
||||
const options: GetNotificationsOptions = { search: containsText };
|
||||
if (unreadOnly !== undefined) {
|
||||
options.read = !unreadOnly;
|
||||
}
|
||||
return api.getNotifications(options);
|
||||
},
|
||||
[containsText, unreadOnly],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,7 +52,6 @@ export const NotificationsPage = () => {
|
||||
}
|
||||
}, [refresh, setRefresh, retry]);
|
||||
|
||||
const { lastSignal } = useSignal('notifications');
|
||||
useEffect(() => {
|
||||
if (lastSignal && lastSignal.action) {
|
||||
setRefresh(true);
|
||||
@@ -63,9 +62,8 @@ export const NotificationsPage = () => {
|
||||
setRefresh(true);
|
||||
};
|
||||
|
||||
const styles = useStyles();
|
||||
if (error) {
|
||||
return <ErrorPanel error={new Error('Failed to load notifications')} />;
|
||||
return <ResponseErrorPanel error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -73,36 +71,21 @@ export const NotificationsPage = () => {
|
||||
<Content>
|
||||
<Grid container>
|
||||
<Grid item xs={2}>
|
||||
<Button
|
||||
className={styles.filterButton}
|
||||
startIcon={<Inbox />}
|
||||
variant={type === 'undone' ? 'contained' : 'text'}
|
||||
onClick={() => setType('undone')}
|
||||
>
|
||||
Inbox
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.filterButton}
|
||||
startIcon={<Check />}
|
||||
variant={type === 'done' ? 'contained' : 'text'}
|
||||
onClick={() => setType('done')}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
<Button
|
||||
className={styles.filterButton}
|
||||
startIcon={<Bookmark />}
|
||||
variant={type === 'saved' ? 'contained' : 'text'}
|
||||
onClick={() => setType('saved')}
|
||||
>
|
||||
Saved
|
||||
</Button>
|
||||
<NotificationsFilters
|
||||
// createdAfter={createdAfter}
|
||||
unreadOnly={unreadOnly}
|
||||
onUnreadOnlyChanged={setUnreadOnly}
|
||||
// onCreatedAfterChanged={setCreatedAfter}
|
||||
// setSorting={setSorting}
|
||||
// sorting={sorting}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
<NotificationsTable
|
||||
isLoading={loading}
|
||||
notifications={value}
|
||||
type={type}
|
||||
onUpdate={onUpdate}
|
||||
setContainsText={setContainsText}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -13,296 +13,162 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from '@material-ui/core';
|
||||
import {
|
||||
Notification,
|
||||
NotificationType,
|
||||
} from '@backstage/plugin-notifications-common';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import Check from '@material-ui/icons/Check';
|
||||
import Bookmark from '@material-ui/icons/Bookmark';
|
||||
import React, { useMemo } from 'react';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { Box, 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 Inbox from '@material-ui/icons/Inbox';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import MarkAsUnreadIcon from '@material-ui/icons/Markunread';
|
||||
import MarkAsReadIcon from '@material-ui/icons/CheckCircle';
|
||||
|
||||
// @ts-ignore
|
||||
import RelativeTime from 'react-relative-time';
|
||||
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
|
||||
import { Link, Table, TableColumn } from '@backstage/core-components';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
table: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
header: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
|
||||
notificationRow: {
|
||||
cursor: 'pointer',
|
||||
'&.unread': {
|
||||
border: '1px solid rgba(255, 255, 255, .3)',
|
||||
},
|
||||
'& .hideOnHover': {
|
||||
display: 'initial',
|
||||
},
|
||||
'& .showOnHover': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
'& .hideOnHover': {
|
||||
display: 'none',
|
||||
},
|
||||
'& .showOnHover': {
|
||||
display: 'initial',
|
||||
},
|
||||
},
|
||||
},
|
||||
actionButton: {
|
||||
padding: '9px',
|
||||
},
|
||||
checkBox: {
|
||||
padding: '0 10px 10px 0',
|
||||
},
|
||||
}));
|
||||
const ThrottleDelayMs = 1000;
|
||||
|
||||
/** @public */
|
||||
export const NotificationsTable = (props: {
|
||||
onUpdate: () => void;
|
||||
type: NotificationType;
|
||||
export type NotificationsTableProps = {
|
||||
isLoading?: boolean;
|
||||
notifications?: Notification[];
|
||||
}) => {
|
||||
const { notifications, type } = props;
|
||||
const navigate = useNavigate();
|
||||
const styles = useStyles();
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
onUpdate: () => void;
|
||||
setContainsText: (search: string) => void;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export const NotificationsTable = ({
|
||||
isLoading,
|
||||
notifications = [],
|
||||
onUpdate,
|
||||
setContainsText,
|
||||
}: NotificationsTableProps) => {
|
||||
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]);
|
||||
}
|
||||
};
|
||||
const onSwitchReadStatus = React.useCallback(
|
||||
(notification: Notification) => {
|
||||
notificationsApi
|
||||
.updateNotifications({
|
||||
ids: [notification.id],
|
||||
read: !notification.read,
|
||||
})
|
||||
.then(() => onUpdate());
|
||||
},
|
||||
[notificationsApi, onUpdate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected([]);
|
||||
}, [type]);
|
||||
const throttledContainsTextHandler = useMemo(
|
||||
() => throttle(setContainsText, ThrottleDelayMs),
|
||||
[setContainsText],
|
||||
);
|
||||
|
||||
const isChecked = (id: string) => {
|
||||
return selected.indexOf(id) !== -1;
|
||||
};
|
||||
|
||||
const isAllSelected = () => {
|
||||
return (
|
||||
selected.length === notifications?.length && notifications.length > 0
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Table size="small" className={styles.table}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell colSpan={3}>
|
||||
{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 === 'done' && selected.length > 0 && (
|
||||
<Button
|
||||
startIcon={<Inbox fontSize="small" />}
|
||||
onClick={() => {
|
||||
notificationsApi
|
||||
.updateNotifications({ ids: selected, done: false })
|
||||
.then(() => props.onUpdate());
|
||||
setSelected([]);
|
||||
}}
|
||||
>
|
||||
Move to inbox
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{type === 'undone' && selected.length > 0 && (
|
||||
<Button
|
||||
startIcon={<Check fontSize="small" />}
|
||||
onClick={() => {
|
||||
notificationsApi
|
||||
.updateNotifications({ ids: selected, done: true })
|
||||
.then(() => props.onUpdate());
|
||||
setSelected([]);
|
||||
}}
|
||||
>
|
||||
Mark as done
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{props.notifications?.map(notification => {
|
||||
const compactColumns = React.useMemo(
|
||||
(): TableColumn<Notification>[] => [
|
||||
{
|
||||
customFilterAndSearch: () =>
|
||||
true /* Keep it on backend due to pagination. If recent flickering is an issue, implement search here as well. */,
|
||||
render: (notification: Notification) => {
|
||||
// Compact content
|
||||
return (
|
||||
<TableRow
|
||||
key={notification.id}
|
||||
className={`${styles.notificationRow} ${
|
||||
!notification.read ? 'unread' : ''
|
||||
}`}
|
||||
hover
|
||||
>
|
||||
<TableCell
|
||||
width="60px"
|
||||
style={{ verticalAlign: 'center', paddingRight: '0px' }}
|
||||
>
|
||||
<Checkbox
|
||||
className={styles.checkBox}
|
||||
size="small"
|
||||
checked={isChecked(notification.id)}
|
||||
onClick={() => onCheckBoxClick(notification.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
onClick={() =>
|
||||
notificationsApi
|
||||
.updateNotifications({ ids: [notification.id], read: true })
|
||||
.then(() => navigate(notification.payload.link))
|
||||
}
|
||||
style={{ paddingLeft: 0 }}
|
||||
>
|
||||
<>
|
||||
<Box>
|
||||
<Typography variant="subtitle2">
|
||||
{notification.payload.title}
|
||||
{notification.payload.link ? (
|
||||
<Link to={notification.payload.link}>
|
||||
{notification.payload.title}
|
||||
</Link>
|
||||
) : (
|
||||
notification.payload.title
|
||||
)}
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
{notification.payload.description}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell style={{ textAlign: 'right' }}>
|
||||
<Box className="hideOnHover">
|
||||
<RelativeTime value={notification.created} />
|
||||
</Box>
|
||||
<Box className="showOnHover">
|
||||
<Tooltip title={notification.payload.link}>
|
||||
<IconButton
|
||||
className={styles.actionButton}
|
||||
onClick={() =>
|
||||
notificationsApi
|
||||
.updateNotifications({
|
||||
ids: [notification.id],
|
||||
read: true,
|
||||
})
|
||||
.then(() => navigate(notification.payload.link))
|
||||
}
|
||||
>
|
||||
<ArrowForwardIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={notification.read ? 'Move to inbox' : 'Mark as done'}
|
||||
>
|
||||
<IconButton
|
||||
className={styles.actionButton}
|
||||
onClick={() => {
|
||||
if (notification.read) {
|
||||
notificationsApi
|
||||
.updateNotifications({
|
||||
ids: [notification.id],
|
||||
done: false,
|
||||
})
|
||||
.then(() => {
|
||||
props.onUpdate();
|
||||
});
|
||||
} else {
|
||||
notificationsApi
|
||||
.updateNotifications({
|
||||
ids: [notification.id],
|
||||
done: true,
|
||||
})
|
||||
.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
|
||||
.updateNotifications({
|
||||
ids: [notification.id],
|
||||
saved: false,
|
||||
})
|
||||
.then(() => {
|
||||
props.onUpdate();
|
||||
});
|
||||
} else {
|
||||
notificationsApi
|
||||
.updateNotifications({
|
||||
ids: [notification.id],
|
||||
saved: true,
|
||||
})
|
||||
.then(() => {
|
||||
props.onUpdate();
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{notification.saved ? (
|
||||
<CloseIcon fontSize="small" />
|
||||
) : (
|
||||
<Bookmark fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<Typography variant="caption">
|
||||
{notification.origin && (
|
||||
<>{notification.origin} • </>
|
||||
)}
|
||||
{notification.payload.topic && (
|
||||
<>{notification.payload.topic} • </>
|
||||
)}
|
||||
{notification.created && (
|
||||
<RelativeTime value={notification.created} />
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
},
|
||||
},
|
||||
// {
|
||||
// // TODO: additional action links
|
||||
// width: '25%',
|
||||
// render: (notification: Notification) => {
|
||||
// return (
|
||||
// notification.payload.link && (
|
||||
// <Grid container>
|
||||
// {/* TODO: render additionalLinks of different titles */}
|
||||
// <Grid item>
|
||||
// <Link
|
||||
// key={notification.payload.link}
|
||||
// to={notification.payload.link}
|
||||
// >
|
||||
// More info
|
||||
// </Link>
|
||||
// </Grid>
|
||||
// </Grid>
|
||||
// )
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
// TODO: action for saving notifications
|
||||
// actions
|
||||
width: '1rem',
|
||||
render: (notification: Notification) => {
|
||||
const markAsReadText = !!notification.read
|
||||
? 'Return among unread'
|
||||
: 'Mark as read';
|
||||
const IconComponent = !!notification.read
|
||||
? MarkAsUnreadIcon
|
||||
: MarkAsReadIcon;
|
||||
|
||||
return (
|
||||
<Tooltip title={markAsReadText}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onSwitchReadStatus(notification);
|
||||
}}
|
||||
>
|
||||
<IconComponent aria-label={markAsReadText} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[onSwitchReadStatus],
|
||||
);
|
||||
|
||||
// TODO: render "Saved notifications" as "Pinned"
|
||||
return (
|
||||
<Table<Notification>
|
||||
isLoading={isLoading}
|
||||
options={{
|
||||
search: true,
|
||||
// TODO: add pagination
|
||||
// paging: true,
|
||||
// pageSize,
|
||||
header: false,
|
||||
sorting: false,
|
||||
}}
|
||||
// onPageChange={setPageNumber}
|
||||
// onRowsPerPageChange={setPageSize}
|
||||
// page={offset}
|
||||
// totalCount={value?.totalCount}
|
||||
onSearchChange={throttledContainsTextHandler}
|
||||
data={notifications}
|
||||
columns={compactColumns}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user