+
+
+ {#if notifications.length > 0}
+
+ {/if}
+
+
+ {#if notifications.length === 0}
+
+
+
+
+
+ {:else}
+
+
+ {#each searchFilter.data as notification (notification.id)}
+
{
+ e.preventDefault();
+
+ routeToOrgDashboard(notification.organisation_id);
+ }}
+ class="overflow-hidden p-0 hover:cursor-pointer"
+ >
+
+
+
+
+
+ {toSentenceCase(notification.type)}
+
+
+ {formatDate(notification.created_at)}
+
+
+
+ {notification.content}
+
+
+
+
+
+
+
+ {/each}
+
+ {/if}
+
diff --git a/frontend/src/routes/(orgs)/notifications/+page.ts b/frontend/src/routes/(orgs)/notifications/+page.ts
new file mode 100644
index 0000000..03671e9
--- /dev/null
+++ b/frontend/src/routes/(orgs)/notifications/+page.ts
@@ -0,0 +1,7 @@
+import type { PageLoad } from "./$types";
+import { notificationSvc } from "$lib/watchtower";
+
+export const load: PageLoad = async () => {
+ const notifications = await notificationSvc.getUnread();
+ return { notifications };
+};
diff --git a/internal/database/notifications.sql.gen.go b/internal/database/notifications.sql.gen.go
index 7b0540e..60764be 100644
--- a/internal/database/notifications.sql.gen.go
+++ b/internal/database/notifications.sql.gen.go
@@ -58,6 +58,7 @@ const deleteOrgNotificationByDate = `-- name: DeleteOrgNotificationByDate :exec
DELETE
FROM organisation_notifications
WHERE created_at < ?
+AND status = 'read'
`
func (q *Queries) DeleteOrgNotificationByDate(ctx context.Context, createdAt int64) error {
@@ -87,15 +88,15 @@ func (q *Queries) GetNotificationByExternalID(ctx context.Context, externalID st
return i, err
}
-const getUnreadNotificationsByOrgID = `-- name: GetUnreadNotificationsByOrgID :many
+const getUnreadNotifications = `-- name: GetUnreadNotifications :many
SELECT id, organisation_id, external_id, type, content, status, created_at, updated_at
FROM organisation_notifications
-WHERE organisation_id = ?
- AND status = 'unread'
+WHERE status = 'unread'
+ORDER BY created_at DESC
`
-func (q *Queries) GetUnreadNotificationsByOrgID(ctx context.Context, organisationID sql.NullInt64) ([]OrganisationNotification, error) {
- rows, err := q.db.QueryContext(ctx, getUnreadNotificationsByOrgID, organisationID)
+func (q *Queries) GetUnreadNotifications(ctx context.Context) ([]OrganisationNotification, error) {
+ rows, err := q.db.QueryContext(ctx, getUnreadNotifications)
if err != nil {
return nil, err
}
diff --git a/internal/database/queries/notifications.sql b/internal/database/queries/notifications.sql
index 0695934..107bc87 100644
--- a/internal/database/queries/notifications.sql
+++ b/internal/database/queries/notifications.sql
@@ -34,13 +34,14 @@ SET status = ?,
WHERE id = ?
RETURNING *;
--- name: GetUnreadNotificationsByOrgID :many
+-- name: GetUnreadNotifications :many
SELECT *
FROM organisation_notifications
-WHERE organisation_id = ?
- AND status = 'unread';
+WHERE status = 'unread'
+ORDER BY created_at DESC;
-- name: DeleteOrgNotificationByDate :exec
DELETE
FROM organisation_notifications
-WHERE created_at < ?;
+WHERE created_at < ?
+AND status = 'read';
diff --git a/internal/notifications/interfaces.go b/internal/notifications/interfaces.go
index 0451957..919912e 100644
--- a/internal/notifications/interfaces.go
+++ b/internal/notifications/interfaces.go
@@ -2,8 +2,6 @@ package notifications
import (
"context"
- "database/sql"
-
"watchtower/internal/database"
)
@@ -12,7 +10,7 @@ type Store interface {
UpdateOrgNotificationByID(ctx context.Context, arg database.UpdateOrgNotificationByIDParams) (database.OrganisationNotification, error)
UpdateOrgNotificationStatusByID(ctx context.Context, arg database.UpdateOrgNotificationStatusByIDParams) (database.OrganisationNotification, error)
GetNotificationByExternalID(ctx context.Context, externalID string) (database.OrganisationNotification, error)
- GetUnreadNotificationsByOrgID(ctx context.Context, organisationID sql.NullInt64) ([]database.OrganisationNotification, error)
+ GetUnreadNotifications(ctx context.Context) ([]database.OrganisationNotification, error)
DeleteOrgNotificationByDate(ctx context.Context, createdAt int64) error
}
diff --git a/internal/notifications/service.go b/internal/notifications/service.go
index 396ff11..8614731 100644
--- a/internal/notifications/service.go
+++ b/internal/notifications/service.go
@@ -50,10 +50,7 @@ func (s *Service) GetUnreadNotifications(ctx context.Context) ([]Notification, e
logger := logging.FromContext(ctx).With("service", "notifications")
logger.Debug("Fetching unread notifications")
- models, err := s.store.GetUnreadNotificationsByOrgID(ctx, sql.NullInt64{
- Int64: 0,
- Valid: true,
- })
+ models, err := s.store.GetUnreadNotifications(ctx)
if err != nil {
logger.Error("Error fetching unread notifications", "error", err)
return []Notification{}, err
diff --git a/internal/notifications/service_test.go b/internal/notifications/service_test.go
index 224b305..86b7217 100644
--- a/internal/notifications/service_test.go
+++ b/internal/notifications/service_test.go
@@ -77,14 +77,19 @@ func TestService(t *testing.T) {
})
odize.AssertNoError(t, err)
+ unread, err := s.GetNotificationByExternalID(ctx, "ext4")
+ odize.AssertNoError(t, err)
+
+ err = s.MarkNotificationAsRead(ctx, unread.ID)
+ odize.AssertNoError(t, err)
+
cutoff := time.Now().Add(1 * time.Minute)
err = s.DeleteNotificationsByDate(ctx, cutoff)
odize.AssertNoError(t, err)
- unread, err := s.GetUnreadNotifications(ctx)
- odize.AssertNoError(t, err)
- odize.AssertEqual(t, 0, len(unread))
+ _, err = s.GetNotificationByExternalID(ctx, "ext4")
+ odize.AssertError(t, err)
}).
Run()
diff --git a/internal/notifications/types.go b/internal/notifications/types.go
index d8339e5..4413558 100644
--- a/internal/notifications/types.go
+++ b/internal/notifications/types.go
@@ -19,11 +19,11 @@ const (
)
type Notification struct {
- ID int64 `json:"id,omitempty"`
- OrganisationID int64 `json:"organisation_id,omitempty"`
- Status NotificationStatus `json:"status,omitempty"`
- Content string `json:"content,omitempty"`
- Type string `json:"type,omitempty"`
+ ID int64 `json:"id"`
+ OrganisationID int64 `json:"organisation_id"`
+ Status NotificationStatus `json:"status"`
+ Content string `json:"content"`
+ Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
diff --git a/internal/watchtower/notifications_test.go b/internal/watchtower/notifications_test.go
index d271ec4..7d1f456 100644
--- a/internal/watchtower/notifications_test.go
+++ b/internal/watchtower/notifications_test.go
@@ -51,15 +51,15 @@ func TestService_Notifications(t *testing.T) {
})
odize.AssertNoError(t, err)
- unread, err := s.GetUnreadNotifications()
+ unread, err := s.notificationSvc.GetNotificationByExternalID(ctx, "test-external-id-2")
odize.AssertNoError(t, err)
- err = s.MarkNotificationAsRead(unread[0].ID)
+ err = s.MarkNotificationAsRead(unread.ID)
odize.AssertNoError(t, err)
- verifyUnread, err := s.GetUnreadNotifications()
+ verifyUnread, err := s.notificationSvc.GetNotificationByExternalID(ctx, "test-external-id-2")
odize.AssertNoError(t, err)
- odize.AssertEqual(t, 0, len(verifyUnread))
+ odize.AssertEqual(t, notifications.StatusRead, verifyUnread.Status)
}).
Test("DeleteOldNotifications should delete notifications", func(t *testing.T) {
orgID := int64(1003)
@@ -71,6 +71,12 @@ func TestService_Notifications(t *testing.T) {
})
odize.AssertNoError(t, err)
+ unread, err := s.notificationSvc.GetNotificationByExternalID(ctx, "test-external-id-3")
+ odize.AssertNoError(t, err)
+
+ err = s.MarkNotificationAsRead(unread.ID)
+ odize.AssertNoError(t, err)
+
// DeleteOldNotifications uses time.Now() and the query uses created_at < ?.
// Since we just created it in the same second, it might not be deleted.
// Let's wait 1 second to ensure created_at < time.Now().
@@ -79,10 +85,8 @@ func TestService_Notifications(t *testing.T) {
err = s.DeleteOldNotifications()
odize.AssertNoError(t, err)
- unread, err := s.GetUnreadNotifications()
- odize.AssertNoError(t, err)
-
- odize.AssertEqual(t, 0, len(unread))
+ _, err = s.notificationSvc.GetNotificationByExternalID(ctx, "test-external-id-3")
+ odize.AssertError(t, err)
}).
Test("CreateUnreadPRNotification should create notifications for recent PRs", func(t *testing.T) {
// Setup: Org, Product, Repo, PR