feat: update Notifications front-end
Signed-off-by: Marek Libra <marek.libra@gmail.com>
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user